diff --git a/GEMINI.md b/GEMINI.md index e7e4d87..63b046f 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,7 +1,7 @@ # GEMINI.md - ModelForge Project Rules & Progress Tracker -> **Last Updated:** 2026-02-17 -> **Status:** Active Development β€” 124 RAG scripts, NotebookLM knowledge integration complete +> **Last Updated:** 2026-02-18 +> **Status:** Active Development β€” 134 RAG scripts, Phase 1 pipeline complete --- @@ -25,7 +25,7 @@ ### Core Features - πŸ€– **AI Orchestration**: ReAct-style planner with per-step validation - πŸ”Œ **Blender MCP Integration**: Socket bridge for executing Python in Blender -- πŸ“š **Hybrid RAG Pipeline**: Context-aware generation using 124 professional Blender scripts +- πŸ“š **Hybrid RAG Pipeline**: Context-aware generation using 134 professional Blender scripts - 🌐 **Web Dashboard**: Project management, auth, conversation history - πŸ–₯️ **Desktop App**: Electron wrapper with native MCP connectivity @@ -38,7 +38,7 @@ ModelForge/ β”œβ”€β”€ app/ # Next.js app directory β”œβ”€β”€ components/ # React components β”œβ”€β”€ data/ -β”‚ └── blender-scripts/ # Library of 124 Python scripts +β”‚ └── blender-scripts/ # Library of 134 Python scripts β”‚ β”œβ”€β”€ tasks/ # Task-specific generators β”‚ └── *.py # Utility modules β”œβ”€β”€ lib/ # Utility libraries @@ -99,7 +99,7 @@ npm run test:user # Create test user | AI Orchestration layer | βœ… Complete | Planner, Executor, Prompts | | **Serverless DB Migration** | βœ… Complete | Neon pgvector compatibility | | **AI Engineering Upgrade** | βœ… Complete | LangChain, Agents, RAG implemented | -| **Script Library Expansion** | βœ… Complete | **124 scripts** (53 utility + 67 tasks + 4 NotebookLM) | +| **Script Library Expansion** | βœ… Complete | **135 scripts** (62 utility + 67 tasks + 4 NotebookLM + 1 research + 1 neural) | | **RAG Pipeline Ingestion** | βœ… Complete | Recursive ingestion of all scripts | | **Viewport Screenshot Analysis** | βœ… Complete | Gemini Vision feedback loop | | **Conversation Memory** | βœ… Complete | Vector embeddings for context-aware responses | @@ -109,6 +109,15 @@ npm run test:user # Create test user | **End-to-End Testing** | βœ… Complete | 3/3 stress tests passed (castle, solar system, edit) | | **Orchestration Hardening** | βœ… Complete | Boolean solver, viewport shading, null safety fixes | | **NotebookLM Knowledge Enhancement** | βœ… Complete | 4 new scripts, 18 API compat categories, 6 prompt sections | +| **Visual Feedback Loop** | βœ… Complete | Post-execution viewport vision with auto-correction | +| **3D Pipeline Strategy** | βœ… Complete | Multi-strategy plan: procedural + neural (open-source) + hybrid | +| **NotebookLM Pipeline Research** | βœ… Complete | Deep research on retopology, Rigify, animation, PBR textures | +| **Phase 1: Production Pipeline RAG** | βœ… Complete | 6 new scripts: retopology, rigging, UV, animation, PBR, export | +| **Phase 2: Neural 3D Layer** | βœ… Complete | 5 providers (Hunyuan Shape/Paint/Part, TRELLIS 2, YVO3D) + hybrid pipeline | +| **Phase 4: AI Strategy Router** | βœ… Complete | Keyword + LLM classification, user override, integrated into chat pipeline | +| **Phase 3: Guided Workflow System** | βœ… Complete | WorkflowAdvisor, per-step UI, workflow-step API, mock neural server | +| **Blender 5.x API Compatibility** | βœ… Complete | 21 breaking change categories in RAG, EEVEE/blend_method/shadow_method fixes | +| **Planner Edit Awareness** | βœ… Complete | Scene state β†’ code gen, structured JSON snapshots, stronger edit rules | ### Roadmap - [x] Gemini-backed conversational planning @@ -121,13 +130,159 @@ npm run test:user # Create test user - [x] **LLM scene completeness validation** - [x] **Stress testing with complex prompts (3/3 passed)** - [x] **NotebookLM knowledge extraction + RAG enhancement** +- [x] **Fine-tuning pipeline (269 training pairs, QLoRA, eval framework)** +- [x] **Visual feedback loop (viewport vision β†’ auto-correct)** +- [x] **3D pipeline strategy (competitors, techniques, 7-phase plan)** +- [x] **Phase 1: RAG scripts (retopology, rigging, animation, UV, PBR, export)** +- [x] **Phase 2: Self-hosted neural 3D layer (Hunyuan Shape/Paint/Part, TRELLIS 2, YVO3D, hybrid pipeline)** +- [ ] Phase 3: Deploy neural models (Azure ML/HF Inference Endpoints) +- [x] **Phase 4: AI strategy router (auto-select procedural vs neural vs hybrid)** +- [x] **Phase 3: Guided workflow system (per-step tool recommendations, human-in-the-loop)** +- [x] **Blender 5.x API compatibility (21 breaking changes, blend_method, EEVEE removals)** +- [x] **Planner edit awareness (scene state β†’ code gen, structured JSON, object referencing)** +- [ ] 🟠 **Tool use guide in system prompt** β€” structured guide for all 14+ MCP commands and how they modify scene state +- [ ] 🟑 **CRAG pipeline** β€” relevance grading + re-ranking for RAG retrieval quality +- [ ] πŸ”΅ Search engine integration (Brave Search API as in-agent tool) β€” only if testing reveals knowledge gaps +- [ ] Phase 5: Credit system + production export pipeline - [ ] Material/color quality enhancement - [ ] Production desktop app packaging +- [ ] 🟠 **CodeRabbit review** β€” address PR review comments from recent commits --- ## πŸ“ Session Log +### 2026-02-21 (Blender 5.x API Fixes + Planner Edit Awareness) +- **Stack Restart Fix**: Next.js 16 with Turbopack wasn't binding to localhost β€” fixed by running `npx next dev -H localhost -p 3000`. OAuth redirect broke with `-H 0.0.0.0` (redirected browser to `0.0.0.0:3000`). +- **Blacksmith Forge Test Analysis** (5 runs total across sessions): + - Runs 1-2: Timeout (60s too short for deep thinking) β†’ fixed to 180s + - Run 3: 6/7 steps passed, step 7 crashed 3x on `eevee.use_ssr` (poisoned by outdated RAG script) + - Run 4: 5/5 steps passed, but step 5 failed 3x: `blend_method = 'ALPHA_BLEND'` (wrong value) + `mat.shadow_method` (removed API) + - Run 5: All steps passed βœ… (after fixes) +- **API Compatibility Fixes**: + - `prompts.ts`: Fixed `ALPHA_BLEND` β†’ `BLEND`, strengthened `shadow_method`/`shadow_mode` AVOID rules + - `api_version_compatibility.py`: Added 3 new sections (19-21): `blend_method` valid values, EEVEE removed properties, `create_transparent_material()` pattern + - `eevee_setup.py`: Rewrote for 5.x β€” removed `use_ssr`, `use_gtao`, `use_bloom`, `taa_render_samples`. Replaced bloom with compositor Glare node. + - `toon_setup.py`: Added `use_nodes = True`, removed `use_ssr`/`shadow_cascade_size` +- **Planner Edit Awareness** (root cause: planner ignored existing objects during edits): + - `executor.ts`: Captures `get_scene_info`/`get_all_object_info` structured data (name, type, location, dimensions) and injects into every `generateCode()` call as `## Current Scene Objects` + - `route.ts`: Scene snapshot now returns structured JSON instead of formatted string. Object cap increased 12 β†’ 30. + - `prompts.ts`: Edit rule 5 now mandates referencing existing objects by exact name + coordinates, never recreating existing objects +- **Re-ingested** all 135 scripts into pgvector after RAG fixes +- **Files Modified**: `lib/ai/prompts.ts`, `lib/orchestration/executor.ts`, `app/api/ai/chat/route.ts`, `data/blender-scripts/api_version_compatibility.py`, `data/blender-scripts/tasks/rendering/eevee_setup.py`, `data/blender-scripts/tasks/rendering/toon_setup.py`, `components/projects/project-chat.tsx` +- **TypeScript**: `tsc --noEmit` passed with 0 errors after all changes +- **Git**: 6 commits pushed to main + +### 2026-02-18 (Phase 3: Guided Workflow System) +- **New Module `lib/orchestration/workflow-types.ts`** β€” `WorkflowStep`, `WorkflowProposal`, `WorkflowStepAction`, `WorkflowStepStatus`, `WorkflowStepResult` types +- **New Module `lib/orchestration/workflow-advisor.ts`** β€” Two-phase workflow generation: + - LLM analysis: Gemini generates per-step tool recommendations (neural/blender_agent/manual) with reasoning + - Static fallback: deterministic categoryβ†’tool mapping for 8 categories (geometry, topology, UV, texturing, rigging, animation, lighting, export) +- **New API `app/api/ai/workflow-step/route.ts`** β€” Per-step execution endpoint: + - Neural step executor (neural client β†’ MCP import) + - Blender agent executor (focused sub-plan β†’ executor) + - Skip/manual_done actions +- **New UI `components/projects/workflow-panel.tsx`** β€” Step cards with: + - Execute/Manual/Skip action buttons + - Category-colored borders, tool recommendation badges + - Progress bar, blocked step indicators, pro tips +- **Chat Route Integration**: Neural/hybrid strategies β†’ `WorkflowProposal` instead of auto-execution; procedural unchanged +- **New Stream Events**: `AgentWorkflowProposal`, `AgentWorkflowStepUpdate` in `types.ts` +- **project-chat.tsx**: Handles `agent:workflow_proposal` event, renders `WorkflowPanel` for last assistant message +- **Mock Neural Server**: `scripts/mock-neural-server.ts` β€” returns valid minimal GLB cube, 1s delay +- **TypeScript**: `tsc --noEmit` passed with 0 errors + +### 2026-02-18 (Phase 4: AI Strategy Router) +- **New Module `lib/orchestration/strategy-router.ts`** β€” Two-phase request classifier: + - Phase 1: Keyword pattern matching (7 procedural, 5 neural, 4 hybrid regex patterns) + - Phase 2: LLM fallback via Gemini for ambiguous requests (structured JSON output) + - User override support (manual strategy selection from UI) + - Confidence scoring (0.0–1.0) with reasoning +- **New Types `lib/orchestration/strategy-types.ts`**: `Strategy`, `StrategyDecision`, `StrategyOverride` +- **Orchestration Integration**: + - `types.ts`: Added `AgentStrategyClassification` stream event + `strategyDecision` in `PlanningMetadata` + - `planner.ts`: Injects neural context into planning prompt when strategy is neural/hybrid + - `executor.ts`: Accepts `strategyDecision` in `ExecutionOptions` + - `route.ts`: Classifies between scene snapshot and planning, emits strategy event to UI +- **TypeScript**: `tsc --noEmit` passed with 0 errors + +### 2026-02-18 (Phase 2: Self-Hosted Neural 3D Layer) +- **New Module `lib/neural/`** β€” 12 files, full abstraction layer for neural 3D generation: + - Core: `types.ts`, `base-client.ts`, `registry.ts`, `index.ts`, `gradio-client.d.ts` + - 5 Provider Clients: + - `providers/hunyuan-shape.ts` β€” Geometry (textβ†’3D, imageβ†’3D, 10GB VRAM) + - `providers/hunyuan-paint.ts` β€” PBR texturing (21GB VRAM) + - `providers/hunyuan-part.ts` β€” Mesh segmentation via Gradio + - `providers/trellis.ts` β€” TRELLIS 2 (Microsoft, MIT, geometry+PBR, 24GB VRAM) + - `providers/yvo3d.ts` β€” Premium texturing API (up to ULTIMA 8K) + - `hybrid-pipeline.ts` β€” 8-stage orchestrator: neural gen β†’ Blender import β†’ retopo β†’ UV β†’ segment β†’ rig β†’ animate β†’ export +- **New RAG Script**: `data/blender-scripts/import_neural_mesh.py` (import, cleanup, normalize, decimate, UV, PBR β€” total: 135 scripts) +- **Prompt Update**: Added neural vs procedural decision rules to `CODE_GENERATION_PROMPT` +- **Orchestration Update**: Added `AgentNeuralGeneration` and `AgentHybridPipeline` stream events to `types.ts` +- **Files Modified**: `lib/ai/prompts.ts`, `lib/orchestration/types.ts` + +### 2026-02-18 (Phase 1: Production Pipeline RAG Scripts) +- **6 New RAG Scripts Created** (total: 134): + - `auto_retopology.py` β€” Voxel remesh, Quadriflow, decimation, mesh repair, full pipeline + - `auto_rigify.py` β€” Rigify metarig templates, rig generation, auto weight painting, bone config + - `auto_uv_unwrap.py` β€” Shape-based auto UV, lightmap UVs, texel density, batch UV, bake UVs + - `procedural_animation.py` β€” Orbit, wave, pendulum, spring, dolly zoom, NLA composition + - `pbr_texture_loader.py` β€” PBR texture loading, folder auto-discovery, displacement, baking + - `model_export.py` β€” LOD generation, format presets (Game/VFX/Web/Print), USD, validation +- **Prompt Update**: Added PRODUCTION PIPELINE section to `CODE_GENERATION_PROMPT` with hints for all 6 capabilities +- **Re-ingested**: 134 scripts into pgvector (new categories: topology:1, export:1; expanded: rigging:4, animation:10, materials:7, uv:2) +- **Files Created**: 6 scripts in `data/blender-scripts/` +- **Files Modified**: `lib/ai/prompts.ts` + +### 2026-02-17 (3D Pipeline Strategy + NotebookLM Deep Research) +- **Comprehensive 3D Pipeline Strategy**: + - Scraped 15+ sources: top3d.ai leaderboard, Rodin CTO interview, Proc3D paper (arXiv), Tripo API docs + - Identified 3 approaches: Neural (diffusion), Procedural (our code-gen), Hybrid (neural + Blender) + - Proc3D paper validates our approach: 89% compile rate (GPT-4o), 98% (fine-tuned LLaMA-3), 400x faster edits + - Rodin CLAY architecture: 3D-native latent diffusion, admits output is "not game-ready" β€” our advantage + - Created `docs/3d-pipeline-strategy.md` β€” competitor analysis, pipeline coverage matrix, revenue model +- **NotebookLM Deep Research** (notebook: ModelForge 3D Pipeline Research): + - Rigify auto-rigging: 61 sources found, 28 imported, comprehensive deep report + - Key APIs: `rigify.generate.generate_rig()`, `parent_set(type='ARMATURE_AUTO')` + - Retopology: `voxel_remesh()` + `quadriflow_remesh()` (single-line APIs) + - Animation: keyframe insertion, F-curve channelbag (Blender 5.0), NLA composition + - Created `docs/research-pipeline-techniques.md` β€” all technique code snippets +- **7-Phase Implementation Plan Defined**: + - Phase 1: RAG scripts (retopology, rigging, animation, UV, textures, export) + - Phase 2: Open-source neural 3D models (Hunyuan 3D, Shap-E, InstantMesh) on Azure ML + - Phase 3: Hybrid pipeline β€” neural β†’ Blender post-processing (THE differentiator) + - Phase 4: Self-hosting inference on Azure ML + - Phase 5: Production game-dev workflow compliance (FBX/glTF/USD) + - Phase 6: AI strategy router (procedural vs neural vs hybrid) + - Phase 7: Credit system + tier pricing +- **Key Decision**: Use open-source models (Hunyuan 3D, Shap-E, Spark, InstantMesh) we can fine-tune β€” NOT competitor APIs (Tripo, Meshy) +- **Files Created**: `docs/3d-pipeline-strategy.md`, `docs/research-pipeline-techniques.md` + +### 2026-02-17 (Visual Feedback Loop + Photorealism RAG) +- **Visual Feedback Loop Wired into Executor**: + - Imported `suggestImprovements` from `lib/ai/vision.ts` into `executor.ts` + - Inserted 140-line correction loop between viewport switch (step 4) and audit (step 5) + - Captures viewport screenshot β†’ Gemini Vision analyzes β†’ generates correction code for high-priority issues + - Max 2 iterations, non-fatal, enabled by default (`enableVisualFeedback: true`) + - Added `AgentVisualAnalysis` and `AgentVisualCorrection` stream event types to `types.ts` + - Enabled flag in `app/api/ai/chat/route.ts` +- **New RAG Scripts**: `hdri_lighting.py`, `photorealistic_materials.py`, `interior_rooms.py` +- **Pitfall Updates**: #16 (noise_scale vs shader node inputs), #17 (camera X rotation) +- **Re-ingested**: 128 scripts, 286 training pairs + +### 2026-02-17 (Fine-Tuning Pipeline + Displacement Textures) +- **Training Data Pipeline**: + - Created `scripts/generate-training-data.ts` β€” parses 125 RAG scripts into instructionβ†’output pairs + - Generated 269 training pairs (125 full-script + 144 function-level) in `training/training_data.jsonl` + - Created `training/eval_prompts.json` β€” 50 held-out test prompts across all categories + - Created `training/train_blender_codegen.py` β€” QLoRA (4-bit NF4) training script for Azure A100 + - Target model: Qwen3-8B, method: QLoRA, focus: Blender code generation only +- **New RAG Script**: `displacement_textures.py` (raked sand, water ripples, rocky terrain, modifier-based displacement) +- **Zen Garden Stress Test**: 7/7 steps, 0 failures, LLM completeness check passed + - Scene: raked sand floor, 3 asymmetric stones, red torii gate, warm area lighting + - Gap found: sand lacked wave/raked texture β†’ fixed with new displacement script +- **Re-ingested**: 125 scripts into pgvector + ### 2026-02-17 (NotebookLM Knowledge Enhancement) - **NotebookLM MCP Integration**: - Installed `notebooklm-mcp-cli` v0.3.2, authenticated with Google diff --git a/app/api/ai/chat/route.ts b/app/api/ai/chat/route.ts index ecf7ed2..6cf010b 100644 --- a/app/api/ai/chat/route.ts +++ b/app/api/ai/chat/route.ts @@ -24,6 +24,8 @@ import type { import { recordExecutionLog } from "@/lib/orchestration/monitor" import { buildSystemPrompt } from "@/lib/orchestration/prompts" import { searchFirecrawl, type FirecrawlSearchResult } from "@/lib/firecrawl" +import { classifyStrategy } from "@/lib/orchestration/strategy-router" +import { generateWorkflowProposal } from "@/lib/orchestration/workflow-advisor" import { z } from "zod" const MAX_HISTORY_MESSAGES = 12 @@ -148,45 +150,40 @@ function formatSceneSnapshot(payload: unknown): string | null { const objectCount = typeof scene.object_count === "number" ? scene.object_count : undefined const errorMessage = typeof scene.error === "string" ? (scene.error as string) : undefined - const objectList = Array.isArray(scene.objects) ? scene.objects.slice(0, 12) : [] - const objects = objectList - .map((raw) => { - if (!raw || typeof raw !== "object") { - return "- (unknown object)" + const objectList = Array.isArray(scene.objects) ? scene.objects.slice(0, 30) : [] + const structuredObjects = objectList + .filter((raw): raw is Record => !!raw && typeof raw === "object") + .map((obj) => { + const entry: Record = { + name: typeof obj.name === "string" ? obj.name : "(unnamed)", + type: typeof obj.type === "string" ? obj.type : "UNKNOWN", } - const obj = raw as Record - const identifier = typeof obj.name === "string" ? obj.name : "(unnamed)" - const type = typeof obj.type === "string" ? obj.type : "UNKNOWN" - const locationArray = Array.isArray(obj.location) ? obj.location : [] - const location = locationArray - .slice(0, 3) - .map((value) => (typeof value === "number" ? value.toFixed(2) : "?")) - .join(", ") - return `- ${identifier} [${type}] @ (${location})` + if (Array.isArray(obj.location)) { + entry.location = obj.location.slice(0, 3).map((v) => typeof v === "number" ? Math.round(v * 100) / 100 : 0) + } + if (Array.isArray(obj.dimensions)) { + entry.dimensions = obj.dimensions.slice(0, 3).map((v) => typeof v === "number" ? Math.round(v * 100) / 100 : 0) + } + return entry }) - .join("\n") - const materials = + const materialsCount = typeof scene.materials_count === "number" - ? `Materials: ${scene.materials_count}` + ? scene.materials_count : undefined - let summary = `Scene: ${name}` - if (typeof objectCount === "number") { - summary += ` | Objects: ${objectCount}` - } - if (materials) { - summary += ` | ${materials}` + // Return structured JSON so the model can parse object names and locations precisely + const snapshot: Record = { + scene: name, + object_count: objectCount, + materials_count: materialsCount, + objects: structuredObjects, } if (errorMessage) { - summary += ` | Error: ${errorMessage}` + snapshot.error = errorMessage } - if (objects) { - summary += `\nObjects:\n${objects}` - } - - return summary + return JSON.stringify(snapshot, null, 2) } @@ -501,94 +498,158 @@ export async function POST(req: Request) { const sceneSnapshotResult = await fetchSceneSummary() + // Strategy classification: determine procedural vs neural vs hybrid + const strategyDecision = await classifyStrategy(message, { + sceneContext: sceneSnapshotResult.summary ?? undefined, + }) + send({ + type: "agent:strategy_classification", + timestamp: new Date().toISOString(), + strategy: strategyDecision.strategy, + confidence: strategyDecision.confidence, + reasoning: strategyDecision.reasoning, + method: strategyDecision.classificationMethod, + }) + let executedCommands: ExecutedCommand[] = [] let planningMetadata: PlanningMetadata | null = null let executionLogs: ExecutionLogEntry[] | undefined = undefined let planResult: PlanGenerationResult | null = null - try { - planResult = await planner.generatePlan( - message, - { - sceneSummary: sceneSnapshotResult.summary ?? undefined, - allowHyper3dAssets: assetConfig.allowHyper3d, - allowSketchfabAssets: assetConfig.allowSketchfab, - allowPolyHavenAssets: assetConfig.allowPolyHaven, - researchContext: researchContext?.promptContext, - }, - llmProvider - ) + // ── Neural/Hybrid β†’ Guided Workflow (human-in-the-loop) ── + if (strategyDecision.strategy === "neural" || strategyDecision.strategy === "hybrid") { + try { + const workflowProposal = await generateWorkflowProposal( + message, + strategyDecision.strategy, + { sceneContext: sceneSnapshotResult.summary ?? undefined } + ) + + // Send the workflow proposal to the UI + send({ + type: "agent:workflow_proposal", + timestamp: new Date().toISOString(), + proposal: workflowProposal, + }) + + // Build a lightweight planningMetadata for the conversation record + planningMetadata = { + planSummary: `Workflow proposed: ${workflowProposal.title} (${workflowProposal.steps.length} steps). Awaiting user action on each step.`, + planSteps: workflowProposal.steps.map((s) => ({ + stepNumber: s.stepNumber, + action: s.recommendedTool === "neural" ? "neural_generate" : s.recommendedTool === "manual" ? "manual" : "execute_code", + parameters: { + category: s.category, + tool: s.recommendedTool, + neuralProvider: s.neuralProvider, + workflowStepId: s.id, + }, + rationale: s.toolReasoning, + expectedOutcome: s.description, + })), + rawPlan: JSON.stringify(workflowProposal, null, 2), + retries: 0, + executionSuccess: true, // Proposal was successfully generated + sceneSnapshot: sceneSnapshotResult.summary, + strategyDecision, + } + } catch (workflowError) { + console.error("Workflow proposal failed, falling back to planner:", workflowError) + // Fall through to the procedural planner+executor below + } + } - if (planResult && planResult.plan) { - const executionResult = await planExecutor.executePlan( - planResult.plan, + // ── Procedural β†’ Auto-pilot planner + executor (existing flow) ── + if (!planningMetadata) { + + try { + planResult = await planner.generatePlan( message, { - ...assetConfig, - onStreamEvent: (event) => send(event), + sceneSummary: sceneSnapshotResult.summary ?? undefined, + allowHyper3dAssets: assetConfig.allowHyper3d, + allowSketchfabAssets: assetConfig.allowSketchfab, + allowPolyHavenAssets: assetConfig.allowPolyHaven, + researchContext: researchContext?.promptContext, + strategyDecision, }, - planResult.analysis, llmProvider ) - executionLogs = executionResult.logs - executedCommands = buildExecutedCommandsFromPlan(planResult.plan, executionResult) - planningMetadata = { - planSummary: planResult.plan.planSummary, - planSteps: planResult.plan.steps, - rawPlan: planResult.rawResponse, - retries: planResult.retries ?? 0, - executionSuccess: executionResult.success, - errors: planResult.errors, - executionLog: executionResult.logs, + + if (planResult && planResult.plan) { + const executionResult = await planExecutor.executePlan( + planResult.plan, + message, + { + ...assetConfig, + enableVisualFeedback: true, + onStreamEvent: (event) => send(event), + strategyDecision, + }, + planResult.analysis, + llmProvider + ) + executionLogs = executionResult.logs + executedCommands = buildExecutedCommandsFromPlan(planResult.plan, executionResult) + planningMetadata = { + planSummary: planResult.plan.planSummary, + planSteps: planResult.plan.steps, + rawPlan: planResult.rawResponse, + retries: planResult.retries ?? 0, + executionSuccess: executionResult.success, + errors: planResult.errors, + executionLog: executionResult.logs, + sceneSnapshot: sceneSnapshotResult.summary, + analysis: planResult.analysis, + researchSummary: researchContext?.promptContext, + researchSources: researchContext?.sources, + strategyDecision, + } + + if (!executionResult.success) { + planningMetadata.executionSuccess = false + } + } else if (planResult) { + const previousLogs = executionLogs + planningMetadata = { + planSummary: "Plan generation failed", + planSteps: [], + rawPlan: planResult.rawResponse, + retries: planResult.retries ?? 0, + executionSuccess: false, + errors: planResult.errors, + fallbackUsed: false, + executionLog: previousLogs, + sceneSnapshot: sceneSnapshotResult.summary, + analysis: planResult.analysis, + researchSummary: researchContext?.promptContext, + researchSources: researchContext?.sources, + } + executedCommands = [] + } else { + throw new Error("Planner returned no result") + } + } catch (error) { + console.error("Planning pipeline error:", error) + const messageText = + error instanceof Error ? error.message : "Unknown planning error" + planningMetadata = planningMetadata ?? { + planSummary: "Planner error", + planSteps: [], + rawPlan: "", + retries: 0, + executionSuccess: false, + errors: [messageText], + fallbackUsed: false, + executionLog: executionLogs, sceneSnapshot: sceneSnapshotResult.summary, - analysis: planResult.analysis, + analysis: planResult?.analysis, researchSummary: researchContext?.promptContext, researchSources: researchContext?.sources, } - - if (!executionResult.success) { - planningMetadata.executionSuccess = false - } - } else if (planResult) { - const previousLogs = executionLogs - planningMetadata = { - planSummary: "Plan generation failed", - planSteps: [], - rawPlan: planResult.rawResponse, - retries: planResult.retries ?? 0, - executionSuccess: false, - errors: planResult.errors, - fallbackUsed: false, - executionLog: previousLogs, - sceneSnapshot: sceneSnapshotResult.summary, - analysis: planResult.analysis, - researchSummary: researchContext?.promptContext, - researchSources: researchContext?.sources, - } executedCommands = [] - } else { - throw new Error("Planner returned no result") } - } catch (error) { - console.error("Planning pipeline error:", error) - const messageText = - error instanceof Error ? error.message : "Unknown planning error" - planningMetadata = planningMetadata ?? { - planSummary: "Planner error", - planSteps: [], - rawPlan: "", - retries: 0, - executionSuccess: false, - errors: [messageText], - fallbackUsed: false, - executionLog: executionLogs, - sceneSnapshot: sceneSnapshotResult.summary, - analysis: planResult?.analysis, - researchSummary: researchContext?.promptContext, - researchSources: researchContext?.sources, - } - executedCommands = [] - } + } // ← closing brace for procedural fallback block diff --git a/app/api/ai/workflow-step/route.ts b/app/api/ai/workflow-step/route.ts new file mode 100644 index 0000000..3c1328d --- /dev/null +++ b/app/api/ai/workflow-step/route.ts @@ -0,0 +1,263 @@ +/** + * Workflow Step Execution API + * + * Handles individual step actions in a guided workflow: + * - execute: Run the step using the recommended tool (neural or Blender agent) + * - skip: Mark the step as skipped + * - manual_done: Mark the step as completed manually by the user + * + * POST /api/ai/workflow-step + * Body: { conversationId, workflowId, stepId, action, userRequest } + */ + +import { NextResponse } from "next/server" +import { auth } from "@/lib/auth" +import { createMcpClient } from "@/lib/mcp" +import { BlenderPlanner } from "@/lib/orchestration/planner" +import { PlanExecutor } from "@/lib/orchestration/executor" +import type { WorkflowStepAction, WorkflowStepResult, WorkflowStep } from "@/lib/orchestration/workflow-types" +import type { LlmProviderSpec } from "@/lib/llm" +import { z } from "zod" + +// --------------------------------------------------------------------------- +// Request validation +// --------------------------------------------------------------------------- + +const RequestSchema = z.object({ + conversationId: z.string(), + workflowId: z.string(), + stepId: z.string(), + action: z.enum(["execute", "skip", "manual_done"]), + /** The original user request (needed for Blender agent steps) */ + userRequest: z.string().optional(), + /** The workflow step data (sent from the UI to avoid re-fetching) */ + step: z.object({ + title: z.string(), + description: z.string(), + recommendedTool: z.enum(["blender_agent", "neural", "manual"]), + category: z.string(), + neuralProvider: z.string().optional(), + }).optional(), +}) + +// --------------------------------------------------------------------------- +// Step executors +// --------------------------------------------------------------------------- + +async function executeNeuralStep( + step: NonNullable["step"]>, + userRequest: string +): Promise { + const startTime = Date.now() + + try { + // Dynamic import to avoid loading neural module when not needed + const { createNeuralClient } = await import("@/lib/neural/registry") + const providerSlug = (step.neuralProvider ?? "hunyuan-shape") as import("@/lib/neural/types").ProviderSlug + + const client = await createNeuralClient(providerSlug) + const result = await client.generate({ + prompt: userRequest, + provider: providerSlug, + mode: "text_to_3d", + }) + + if (result.status === "completed" && result.modelPath) { + // Import into Blender via MCP + const mcpClient = createMcpClient() + try { + const importCode = ` +import bpy + +# Import the neural-generated mesh +bpy.ops.import_scene.gltf(filepath=r"${result.modelPath.replace(/\\/g, "\\\\")}") + +# Center and normalize +imported = bpy.context.selected_objects +if imported: + obj = imported[0] + obj.name = "NeuralMesh_${step.category}" + bpy.context.view_layer.objects.active = obj + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') + obj.location = (0, 0, 0) + print(f"Imported neural mesh: {obj.name}") +` + await mcpClient.execute({ type: "execute_code", params: { code: importCode } }) + } finally { + await mcpClient.close() + } + + return { + stepId: "", + status: "completed", + message: `Neural mesh generated and imported into Blender (${result.provider})`, + outputPath: result.modelPath, + durationMs: Date.now() - startTime, + } + } + + return { + stepId: "", + status: "failed", + error: result.error ?? "Neural generation returned no output", + durationMs: Date.now() - startTime, + } + } catch (error) { + return { + stepId: "", + status: "failed", + error: error instanceof Error ? error.message : "Neural step execution failed", + durationMs: Date.now() - startTime, + } + } +} + +async function executeBlenderAgentStep( + step: NonNullable["step"]>, + userRequest: string, + llmProvider?: LlmProviderSpec +): Promise { + const startTime = Date.now() + + try { + // Create a focused sub-request for this specific step + const focusedRequest = `${step.description}. Context: the user is working on "${userRequest}". Focus only on this specific task: ${step.title}.` + + const mcpClient = createMcpClient() + + try { + const planner = new BlenderPlanner() + const planResult = await planner.generatePlan( + focusedRequest, + { + allowHyper3dAssets: false, + allowSketchfabAssets: false, + allowPolyHavenAssets: true, + }, + llmProvider + ) + + if (!planResult?.plan) { + return { + stepId: "", + status: "failed", + error: "Failed to generate a plan for this step", + durationMs: Date.now() - startTime, + } + } + + const executor = new PlanExecutor() + const executionResult = await executor.executePlan( + planResult.plan, + focusedRequest, + { + allowHyper3d: false, + allowSketchfab: false, + allowPolyHaven: true, + enableVisualFeedback: true, + }, + planResult.analysis, + llmProvider + ) + + return { + stepId: "", + status: executionResult.success ? "completed" : "failed", + message: executionResult.success + ? `Blender agent completed: ${step.title}` + : undefined, + error: executionResult.success ? undefined : "Blender agent execution failed", + durationMs: Date.now() - startTime, + } + } finally { + await mcpClient.close() + } + } catch (error) { + return { + stepId: "", + status: "failed", + error: error instanceof Error ? error.message : "Blender agent step failed", + durationMs: Date.now() - startTime, + } + } +} + +// --------------------------------------------------------------------------- +// POST handler +// --------------------------------------------------------------------------- + +export async function POST(request: Request) { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } + + let body: unknown + try { + body = await request.json() + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }) + } + + const parsed = RequestSchema.safeParse(body) + if (!parsed.success) { + return NextResponse.json( + { error: "Invalid request", details: parsed.error.format() }, + { status: 400 } + ) + } + + const { stepId, action, step, userRequest } = parsed.data + + // ── Skip / Manual Done ── + if (action === "skip") { + const result: WorkflowStepResult = { + stepId, + status: "skipped", + message: "Step skipped by user", + } + return NextResponse.json(result) + } + + if (action === "manual_done") { + const result: WorkflowStepResult = { + stepId, + status: "manual", + message: "Step completed manually by user", + } + return NextResponse.json(result) + } + + // ── Execute ── + if (!step) { + return NextResponse.json( + { error: "Step data is required for execution" }, + { status: 400 } + ) + } + + if (!userRequest) { + return NextResponse.json( + { error: "userRequest is required for execution" }, + { status: 400 } + ) + } + + let result: WorkflowStepResult + + if (step.recommendedTool === "neural") { + result = await executeNeuralStep(step, userRequest) + } else if (step.recommendedTool === "blender_agent") { + result = await executeBlenderAgentStep(step, userRequest) + } else { + result = { + stepId, + status: "manual", + message: "This step is recommended for manual execution in Blender.", + } + } + + result.stepId = stepId + + return NextResponse.json(result) +} diff --git a/components/projects/project-chat.tsx b/components/projects/project-chat.tsx index 8dd24db..3aef9c4 100644 --- a/components/projects/project-chat.tsx +++ b/components/projects/project-chat.tsx @@ -11,6 +11,8 @@ import type { UsageSummary } from "@/lib/usage" import type { PlanningMetadata, PlanStep, AgentStreamEvent } from "@/lib/orchestration/types" import { parsePlanningMetadata } from "@/lib/orchestration/plan-utils" import { ImagePlus, X } from "lucide-react" +import { WorkflowPanel } from "@/components/projects/workflow-panel" +import type { WorkflowProposal } from "@/lib/orchestration/workflow-types" interface CommandStub { id: string @@ -104,6 +106,9 @@ export function ProjectChat({ const [input, setInput] = useState("") const [isSending, setIsSending] = useState(false) const [error, setError] = useState(null) + const [isRetryable, setIsRetryable] = useState(false) + const [lastPayload, setLastPayload] = useState | null>(null) + const lastTempAssistantIdRef = useRef(null) const localProviderConfigured = Boolean( localProvider.provider && localProvider.baseUrl && localProvider.model ) @@ -118,6 +123,7 @@ export function ProjectChat({ const [localReady, setLocalReady] = useState(localProviderConfigured) const [agentEvents, setAgentEvents] = useState([]) const [agentActive, setAgentActive] = useState(false) + const [activeWorkflow, setActiveWorkflow] = useState(null) const [mcpConnected, setMcpConnected] = useState(null) const MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024 const MAX_ATTACHMENTS = 4 @@ -251,155 +257,155 @@ export function ProjectChat({ return history.some((item) => item.id === conversationId) }, [conversationId, history]) -function handleLoadConversation(conversation: ConversationHistoryItem) { - setConversationId(conversation.id) - setMessages(conversation.messages.map((msg) => ({ ...msg }))) - setError(null) - setInput("") - setIsSending(false) - setAttachments([]) - setAgentEvents([]) - setAgentActive(false) -} - -const handleAttachmentButton = () => { - fileInputRef.current?.click() -} - -const readFileAsDataUrl = (file: File) => - new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = () => resolve(reader.result as string) - reader.onerror = () => reject(reader.error) - reader.readAsDataURL(file) - }) - -const handleFileInputChange = async (event: ChangeEvent) => { - const files = event.target.files - if (!files || files.length === 0) { - return + function handleLoadConversation(conversation: ConversationHistoryItem) { + setConversationId(conversation.id) + setMessages(conversation.messages.map((msg) => ({ ...msg }))) + setError(null) + setInput("") + setIsSending(false) + setAttachments([]) + setAgentEvents([]) + setAgentActive(false) } - const remainingSlots = MAX_ATTACHMENTS - attachments.length - if (remainingSlots <= 0) { - setError(`You can attach up to ${MAX_ATTACHMENTS} images per message.`) - event.target.value = "" - return + const handleAttachmentButton = () => { + fileInputRef.current?.click() } - const selectedFiles = Array.from(files).slice(0, remainingSlots) - const newAttachments: PendingAttachment[] = [] + const readFileAsDataUrl = (file: File) => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsDataURL(file) + }) - for (const file of selectedFiles) { - if (!file.type.startsWith("image/")) { - setError("Only image files are supported right now.") - continue - } - if (file.size > MAX_ATTACHMENT_SIZE) { - setError("Images must be 5MB or smaller.") - continue + const handleFileInputChange = async (event: ChangeEvent) => { + const files = event.target.files + if (!files || files.length === 0) { + return } - try { - const dataUrl = await readFileAsDataUrl(file) - const base64 = dataUrl.split(",")[1] ?? "" - newAttachments.push({ - id: crypto.randomUUID(), - name: file.name, - size: file.size, - type: file.type, - dataUrl, - base64, - }) - } catch (fileError) { - console.error(fileError) - setError("Failed to read one of the files. Please try again.") + const remainingSlots = MAX_ATTACHMENTS - attachments.length + if (remainingSlots <= 0) { + setError(`You can attach up to ${MAX_ATTACHMENTS} images per message.`) + event.target.value = "" + return } - } - if (newAttachments.length > 0) { - setAttachments((prev) => [...prev, ...newAttachments]) - setError(null) - } + const selectedFiles = Array.from(files).slice(0, remainingSlots) + const newAttachments: PendingAttachment[] = [] - event.target.value = "" -} + for (const file of selectedFiles) { + if (!file.type.startsWith("image/")) { + setError("Only image files are supported right now.") + continue + } + if (file.size > MAX_ATTACHMENT_SIZE) { + setError("Images must be 5MB or smaller.") + continue + } -const handleRemoveAttachment = (id: string) => { - setAttachments((prev) => prev.filter((attachment) => attachment.id !== id)) -} + try { + const dataUrl = await readFileAsDataUrl(file) + const base64 = dataUrl.split(",")[1] ?? "" + newAttachments.push({ + id: crypto.randomUUID(), + name: file.name, + size: file.size, + type: file.type, + dataUrl, + base64, + }) + } catch (fileError) { + console.error(fileError) + setError("Failed to read one of the files. Please try again.") + } + } -const handleProviderChange = (event: ChangeEvent) => { - const value = event.target.value - if (value === "configure") { - router.push("/dashboard/settings#local-llm") - return - } - if (!value) { - return - } - if (value === localProvider.provider) { - return + if (newAttachments.length > 0) { + setAttachments((prev) => [...prev, ...newAttachments]) + setError(null) + } + + event.target.value = "" } - router.push(`/dashboard/settings?localProvider=${value}#local-llm`) -} -async function handleSend(e: React.FormEvent) { - e.preventDefault() - if (!canSend) return - - const trimmed = input.trim() - const now = new Date().toISOString() - const tempUserId = `temp-user-${Date.now()}` - const tempAssistantId = `temp-assistant-${Date.now()}` - const draftAttachments: ChatAttachment[] = attachments.map((attachment) => ({ - id: attachment.id, - name: attachment.name, - type: attachment.type, - size: attachment.size, - previewUrl: attachment.dataUrl, - })) + const handleRemoveAttachment = (id: string) => { + setAttachments((prev) => prev.filter((attachment) => attachment.id !== id)) + } - setIsSending(true) - setError(null) - setInput("") - setMessages((prev) => [ - ...prev, - { - id: tempUserId, - role: "user", - content: trimmed, - createdAt: now, - attachments: draftAttachments, - }, - { - id: tempAssistantId, - role: "assistant", - content: "", - createdAt: now, - mcpCommands: [], - }, - ]) - setAttachments([]) - - try { - if (subscriptionTier === "free" && !localReady) { - setError( - "The free tier requires a local LLM. Configure one in Settings β†’ Local LLM Configuration before prompting." - ) + const handleProviderChange = (event: ChangeEvent) => { + const value = event.target.value + if (value === "configure") { + router.push("/dashboard/settings#local-llm") return } - - const payload: Record = { - projectId, - conversationId: conversationId ?? undefined, - startNew: !conversationId, - message: trimmed, + if (!value) { + return } - - if (subscriptionTier === "free") { - payload.useLocalModel = true + if (value === localProvider.provider) { + return } + router.push(`/dashboard/settings?localProvider=${value}#local-llm`) + } + + async function handleSend(e: React.FormEvent) { + e.preventDefault() + if (!canSend) return + + const trimmed = input.trim() + const now = new Date().toISOString() + const tempUserId = `temp-user-${Date.now()}` + const tempAssistantId = `temp-assistant-${Date.now()}` + const draftAttachments: ChatAttachment[] = attachments.map((attachment) => ({ + id: attachment.id, + name: attachment.name, + type: attachment.type, + size: attachment.size, + previewUrl: attachment.dataUrl, + })) + + setIsSending(true) + setError(null) + setInput("") + setMessages((prev) => [ + ...prev, + { + id: tempUserId, + role: "user", + content: trimmed, + createdAt: now, + attachments: draftAttachments, + }, + { + id: tempAssistantId, + role: "assistant", + content: "", + createdAt: now, + mcpCommands: [], + }, + ]) + setAttachments([]) + + try { + if (subscriptionTier === "free" && !localReady) { + setError( + "The free tier requires a local LLM. Configure one in Settings β†’ Local LLM Configuration before prompting." + ) + return + } + + const payload: Record = { + projectId, + conversationId: conversationId ?? undefined, + startNew: !conversationId, + message: trimmed, + } + + if (subscriptionTier === "free") { + payload.useLocalModel = true + } if (attachments.length > 0) { payload.attachments = attachments.map((attachment) => ({ @@ -411,12 +417,18 @@ async function handleSend(e: React.FormEvent) { })) } + // Save payload for potential retry + setLastPayload(payload) + lastTempAssistantIdRef.current = tempAssistantId + + const abortController = new AbortController() const response = await fetch("/api/ai/chat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), + signal: abortController.signal, }) if (!response.ok || !response.body) { @@ -429,6 +441,8 @@ async function handleSend(e: React.FormEvent) { const errorMessage = typeof data?.error === "string" ? data.error : "Failed to send message" setError(errorMessage) + // Server errors (5xx) are retryable, client errors (4xx) are not + setIsRetryable(response.status >= 500) if ( subscriptionTier === "free" && typeof data?.error === "string" && @@ -452,112 +466,132 @@ async function handleSend(e: React.FormEvent) { let assistantContent = "" let streamFinished = false - while (!streamFinished) { - const { done, value } = await reader.read() - if (done) { - streamFinished = true - } - buffer += decoder.decode(value ?? new Uint8Array(), { - stream: !done, - }) + // Stream stall timeout β€” abort if no data received within this window. + // Deep-thinking models (Gemini 3.1) can take 60-120s per reasoning phase. + const STREAM_STALL_TIMEOUT_MS = + Number(process.env.NEXT_PUBLIC_STREAM_STALL_TIMEOUT_MS) || 180_000 + let staleTimer: ReturnType | null = null - let newlineIndex: number - while ((newlineIndex = buffer.indexOf("\n")) !== -1) { - const line = buffer.slice(0, newlineIndex).trim() - buffer = buffer.slice(newlineIndex + 1) + const resetStaleTimer = () => { + if (staleTimer) clearTimeout(staleTimer) + staleTimer = setTimeout(() => { + abortController.abort() + }, STREAM_STALL_TIMEOUT_MS) + } - if (!line) { - continue - } + // Start the initial timer + resetStaleTimer() - let event: Record - try { - event = JSON.parse(line) as Record - } catch { - continue + try { + while (!streamFinished) { + const { done, value } = await reader.read() + if (done) { + streamFinished = true + } else { + // Reset stall timer on each chunk received + resetStaleTimer() } + buffer += decoder.decode(value ?? new Uint8Array(), { + stream: !done, + }) - const eventType = typeof event.type === "string" ? event.type : undefined - if (!eventType) { - continue - } + let newlineIndex: number + while ((newlineIndex = buffer.indexOf("\n")) !== -1) { + const line = buffer.slice(0, newlineIndex).trim() + buffer = buffer.slice(newlineIndex + 1) - switch (eventType) { - case "init": { - const incomingConversationId = - typeof event.conversationId === "string" - ? event.conversationId - : undefined - if (incomingConversationId) { - setConversationId(incomingConversationId) - } - break + if (!line) { + continue } - case "delta": { - const delta = typeof event.delta === "string" ? event.delta : "" - assistantContent += delta - setMessages((prev) => - prev.map((msg) => - msg.id === tempAssistantId - ? { ...msg, content: assistantContent } - : msg - ) - ) - break + + let event: Record + try { + event = JSON.parse(line) as Record + } catch { + continue } - case "usage": { - const usagePayload = event.usage as UsageSummary | undefined - if (usagePayload) { - setUsage(usagePayload) - } - break + + const eventType = typeof event.type === "string" ? event.type : undefined + if (!eventType) { + continue } - case "complete": { - const messagesPayload = Array.isArray(event.messages) - ? (event.messages as Array>) - : [] - const userRecordRaw = messagesPayload[0] - const assistantRecordRaw = messagesPayload[1] - const suggestionPayload = Array.isArray(event.commandSuggestions) - ? (event.commandSuggestions as CommandStub[]) - : undefined - const planPayload = parsePlanningMetadata(event.planning) - - const completedConversationId = - typeof event.conversationId === "string" - ? event.conversationId - : undefined - if (completedConversationId) { - setConversationId(completedConversationId) - } - if (userRecordRaw && typeof userRecordRaw === "object") { - const userRecord = userRecordRaw as Partial + switch (eventType) { + case "init": { + const incomingConversationId = + typeof event.conversationId === "string" + ? event.conversationId + : undefined + if (incomingConversationId) { + setConversationId(incomingConversationId) + } + break + } + case "delta": { + const delta = typeof event.delta === "string" ? event.delta : "" + assistantContent += delta setMessages((prev) => prev.map((msg) => - msg.id === tempUserId - ? { - ...msg, - id: userRecord.id ?? msg.id, - createdAt: userRecord.createdAt ?? msg.createdAt, - } + msg.id === tempAssistantId + ? { ...msg, content: assistantContent } : msg ) ) + break } + case "usage": { + const usagePayload = event.usage as UsageSummary | undefined + if (usagePayload) { + setUsage(usagePayload) + } + break + } + case "complete": { + const messagesPayload = Array.isArray(event.messages) + ? (event.messages as Array>) + : [] + const userRecordRaw = messagesPayload[0] + const assistantRecordRaw = messagesPayload[1] + const suggestionPayload = Array.isArray(event.commandSuggestions) + ? (event.commandSuggestions as CommandStub[]) + : undefined + const planPayload = parsePlanningMetadata(event.planning) - let assistantRecordId: string | undefined - if (assistantRecordRaw && typeof assistantRecordRaw === "object") { - const assistantRecord = assistantRecordRaw as Partial & { - mcpCommands?: CommandStub[] + const completedConversationId = + typeof event.conversationId === "string" + ? event.conversationId + : undefined + if (completedConversationId) { + setConversationId(completedConversationId) } - assistantContent = - assistantRecord.content ?? assistantContent - assistantRecordId = assistantRecord.id - setMessages((prev) => - prev.map((msg) => - msg.id === tempAssistantId - ? { + + if (userRecordRaw && typeof userRecordRaw === "object") { + const userRecord = userRecordRaw as Partial + setMessages((prev) => + prev.map((msg) => + msg.id === tempUserId + ? { + ...msg, + id: userRecord.id ?? msg.id, + createdAt: userRecord.createdAt ?? msg.createdAt, + } + : msg + ) + ) + } + + let assistantRecordId: string | undefined + if (assistantRecordRaw && typeof assistantRecordRaw === "object") { + const assistantRecord = assistantRecordRaw as Partial & { + mcpCommands?: CommandStub[] + } + assistantContent = + assistantRecord.content ?? assistantContent + assistantRecordId = assistantRecord.id + setMessages((prev) => + prev.map((msg) => + msg.id === tempAssistantId + ? { ...msg, id: assistantRecord.id ?? msg.id, content: @@ -570,93 +604,143 @@ async function handleSend(e: React.FormEvent) { : suggestionPayload, plan: planPayload ?? assistantRecord.plan ?? msg.plan, } - : msg + : msg + ) ) - ) - } + } - const usagePayload = event.usage as UsageSummary | undefined - if (usagePayload) { - setUsage(usagePayload) - } + const usagePayload = event.usage as UsageSummary | undefined + if (usagePayload) { + setUsage(usagePayload) + } - if ((suggestionPayload || planPayload) && completedConversationId) { - setHistory((prev) => - prev.map((item) => - item.id === completedConversationId - ? { + if ((suggestionPayload || planPayload) && completedConversationId) { + setHistory((prev) => + prev.map((item) => + item.id === completedConversationId + ? { ...item, messages: item.messages.map((msg) => assistantRecordId && msg.id === assistantRecordId ? { - ...msg, - mcpCommands: suggestionPayload ?? msg.mcpCommands, - plan: planPayload ?? msg.plan, - } + ...msg, + mcpCommands: suggestionPayload ?? msg.mcpCommands, + plan: planPayload ?? msg.plan, + } : msg ), } - : item + : item + ) ) + } + + router.refresh() + streamFinished = true + setAgentActive(false) + break + } + case "error": { + const errorMessage = + typeof event.error === "string" + ? event.error + : "Failed to process AI request" + setError(errorMessage) + setAgentActive(false) + setMessages((prev) => + prev.filter((msg) => msg.id !== tempAssistantId) ) + streamFinished = true + break } - - router.refresh() - streamFinished = true - setAgentActive(false) - break - } - case "error": { - const errorMessage = - typeof event.error === "string" - ? event.error - : "Failed to process AI request" - setError(errorMessage) - setAgentActive(false) - setMessages((prev) => - prev.filter((msg) => msg.id !== tempAssistantId) - ) - streamFinished = true - break - } - default: - // Handle agent stream events - if (eventType.startsWith("agent:")) { - const agentEvent: AgentStreamEvent | null = - event && typeof event === "object" && "type" in event && typeof event.type === "string" && event.type.startsWith("agent:") - ? (event as unknown as AgentStreamEvent) - : null - if (!agentEvent) break - if (agentEvent.type === "agent:planning_start") { - setAgentActive(true) - setAgentEvents([agentEvent]) - } else if (agentEvent.type === "agent:complete") { - setAgentEvents((prev) => [...prev, agentEvent]) - // Keep active briefly so user can see the final status - } else { - setAgentEvents((prev) => [...prev, agentEvent]) + default: + // Handle agent stream events + if (eventType.startsWith("agent:")) { + const agentEvent: AgentStreamEvent | null = + event && typeof event === "object" && "type" in event && typeof event.type === "string" && event.type.startsWith("agent:") + ? (event as unknown as AgentStreamEvent) + : null + if (!agentEvent) break + if (agentEvent.type === "agent:planning_start") { + setAgentActive(true) + setAgentEvents([agentEvent]) + } else if (agentEvent.type === "agent:workflow_proposal") { + setActiveWorkflow((agentEvent as unknown as { proposal: WorkflowProposal }).proposal) + setAgentEvents((prev) => [...prev, agentEvent]) + } else if (agentEvent.type === "agent:complete") { + setAgentEvents((prev) => [...prev, agentEvent]) + // Keep active briefly so user can see the final status + } else { + setAgentEvents((prev) => [...prev, agentEvent]) + } } - } - break + break + } } } + } finally { + // Clear the stall timer when streaming ends (success or error) + if (staleTimer) clearTimeout(staleTimer) } } catch (err) { - setError( - err instanceof Error ? err.message : "Something went wrong. Try again." - ) - setMessages((prev) => - prev.filter((msg) => msg.id !== tempAssistantId) + // Classify errors as retryable vs non-retryable + const isAbort = err instanceof DOMException && err.name === "AbortError" + const isNetworkError = err instanceof TypeError && err.message.includes("fetch") + const isStreamError = err instanceof Error && ( + err.message.includes("network") || + err.message.includes("abort") || + err.message.includes("Failed to fetch") || + err.message.includes("The operation was aborted") ) + const retryable = isAbort || isNetworkError || isStreamError + + const errorMessage = isAbort + ? "Connection timed out β€” the server stopped responding. Your request may still be processing." + : err instanceof Error + ? err.message + : "Something went wrong. Try again." + + setError(errorMessage) + setIsRetryable(retryable) + + if (!retryable) { + // Non-retryable: remove the assistant message + setMessages((prev) => + prev.filter((msg) => msg.id !== tempAssistantId) + ) + } + // Retryable: keep assistant message (shows partial progress) } finally { setIsSending(false) } } + /** + * Retry the last failed request. + * Re-sends the saved payload to /api/ai/chat. + */ + function handleRetry() { + if (!lastPayload || isSending) return + setError(null) + setIsRetryable(false) + + // Create a synthetic form event and reinvoke handleSend + // But we need the same payload, so we restore input and call handleSend + const message = typeof lastPayload.message === "string" ? lastPayload.message : "" + setInput(message) + // Use a microtask so the state update takes effect before handleSend reads it + setTimeout(() => { + const form = document.querySelector('form') + if (form) form.requestSubmit() + }, 0) + } + function handleStartNew() { setConversationId(null) setMessages([]) setError(null) + setIsRetryable(false) + setLastPayload(null) setInput("") setAttachments([]) setAgentEvents([]) @@ -851,24 +935,21 @@ async function handleSend(e: React.FormEvent) { messages.map((message, index) => (
{message.content}
{message.attachments?.length ? (
{message.attachments.map((attachment) => { const previewSrc = attachment.previewUrl ?? attachment.url @@ -1099,6 +1180,19 @@ async function handleSend(e: React.FormEvent) { )}
)} + {/* Guided workflow panel (neural/hybrid requests) */} + {message.role === "assistant" && + index === messages.length - 1 && + activeWorkflow && ( +
+ { + console.log(`[Workflow] Step ${stepId}: ${action}`) + }} + /> +
+ )} {message.role === "assistant" && Array.isArray(message.mcpCommands) && message.mcpCommands.length > 0 && ( @@ -1232,8 +1326,20 @@ async function handleSend(e: React.FormEvent) { )} {error && ( -
- {error} +
+ {error} + {isRetryable && ( + + )}
)}
diff --git a/components/projects/workflow-panel.tsx b/components/projects/workflow-panel.tsx new file mode 100644 index 0000000..26c7198 --- /dev/null +++ b/components/projects/workflow-panel.tsx @@ -0,0 +1,376 @@ +"use client" + +import { useState, useCallback } from "react" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import type { + WorkflowProposal, + WorkflowStep, + WorkflowStepStatus, + WorkflowStepAction, + WorkflowStepResult, +} from "@/lib/orchestration/workflow-types" + +// ─── Icons for each tool ────────────────────────────────────────── + +const TOOL_ICONS: Record = { + neural: "🧠", + blender_agent: "πŸ€–", + manual: "βœ‹", +} + +const TOOL_LABELS: Record = { + neural: "Neural AI", + blender_agent: "Blender Agent", + manual: "Manual", +} + +const STATUS_CONFIG: Record< + WorkflowStepStatus, + { label: string; variant: "default" | "secondary" | "outline" | "destructive"; icon: string } +> = { + pending: { label: "Pending", variant: "outline", icon: "β—‹" }, + running: { label: "Running", variant: "secondary", icon: "⏳" }, + completed: { label: "Completed", variant: "default", icon: "βœ…" }, + failed: { label: "Failed", variant: "destructive", icon: "❌" }, + skipped: { label: "Skipped", variant: "outline", icon: "⏭" }, + manual: { label: "Done Manually", variant: "secondary", icon: "βœ‹" }, +} + +const CATEGORY_COLORS: Record = { + geometry: "border-blue-500/30 bg-blue-500/5", + topology: "border-green-500/30 bg-green-500/5", + uv: "border-yellow-500/30 bg-yellow-500/5", + texturing: "border-purple-500/30 bg-purple-500/5", + rigging: "border-orange-500/30 bg-orange-500/5", + animation: "border-pink-500/30 bg-pink-500/5", + lighting: "border-amber-500/30 bg-amber-500/5", + export: "border-cyan-500/30 bg-cyan-500/5", + composition: "border-teal-500/30 bg-teal-500/5", + other: "border-border/60 bg-muted/10", +} + +// ─── Props ──────────────────────────────────────────────────────── + +interface WorkflowPanelProps { + proposal: WorkflowProposal + /** Called when user takes action on any step */ + onStepAction?: (stepId: string, action: WorkflowStepAction) => void +} + +// ─── Step Card Component ──────────────────────────────────────── + +function StepCard({ + step, + isBlocked, + onAction, + isExecuting, +}: { + step: WorkflowStep & { status: WorkflowStepStatus } + isBlocked: boolean + onAction: (action: WorkflowStepAction) => void + isExecuting: boolean +}) { + const status = STATUS_CONFIG[step.status] + const categoryColor = CATEGORY_COLORS[step.category] ?? CATEGORY_COLORS.other + const toolIcon = TOOL_ICONS[step.recommendedTool] ?? "πŸ”§" + const toolLabel = TOOL_LABELS[step.recommendedTool] ?? step.recommendedTool + + const isActionable = step.status === "pending" && !isBlocked && !isExecuting + const isRunning = step.status === "running" + + return ( +
+ {/* Header */} +
+
+ + {step.stepNumber} + + + {step.title} + +
+
+ + {status.icon} {status.label} + +
+
+ + {/* Description */} +

{step.description}

+ + {/* Tool recommendation */} +
+ {toolIcon} +
+ + Recommended: {toolLabel} + +

+ {step.toolReasoning} +

+
+
+ + {/* Alternative tools */} + {step.alternativeTools.length > 0 && ( +

+ Alternatives:{" "} + {step.alternativeTools + .map((t) => `${TOOL_ICONS[t] ?? ""} ${TOOL_LABELS[t] ?? t}`) + .join(", ")} +

+ )} + + {/* Pro tip */} + {step.tips && ( +
+ πŸ’‘ {step.tips} +
+ )} + + {/* Duration estimate */} +
+ ⏱ {step.estimatedDuration} + {step.category} + {step.neuralProvider && ( + Provider: {step.neuralProvider} + )} +
+ + {/* Error message */} + {step.error && ( +
+ {step.error} +
+ )} + + {/* Action buttons */} + {isActionable && ( +
+ + + +
+ )} + + {/* Running spinner */} + {isRunning && ( +
+ ⏳ + Executing step... +
+ )} + + {/* Blocked */} + {step.status === "pending" && isBlocked && !isExecuting && ( +
+ ⏸ Waiting for previous step to complete +
+ )} +
+ ) +} + +// ─── Main Workflow Panel ────────────────────────────────────────── + +export function WorkflowPanel({ proposal, onStepAction }: WorkflowPanelProps) { + const [steps, setSteps] = useState<(WorkflowStep & { status: WorkflowStepStatus })[]>( + proposal.steps.map((s) => ({ ...s, status: s.status ?? "pending" })) + ) + const [executingStepId, setExecutingStepId] = useState(null) + + const completedCount = steps.filter( + (s) => s.status === "completed" || s.status === "skipped" || s.status === "manual" + ).length + const progress = steps.length > 0 ? Math.round((completedCount / steps.length) * 100) : 0 + + const handleStepAction = useCallback( + async (stepId: string, action: WorkflowStepAction) => { + const stepIndex = steps.findIndex((s) => s.id === stepId) + if (stepIndex === -1) return + + const step = steps[stepIndex] + + // Optimistic update for skip and manual + if (action === "skip" || action === "manual_done") { + setSteps((prev) => + prev.map((s) => + s.id === stepId + ? { ...s, status: action === "skip" ? "skipped" as const : "manual" as const } + : s + ) + ) + onStepAction?.(stepId, action) + // Fire API call in background + fetch("/api/ai/workflow-step", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + conversationId: "", + workflowId: proposal.id, + stepId, + action, + }), + }).catch(console.error) + return + } + + // Execute action + setExecutingStepId(stepId) + setSteps((prev) => + prev.map((s) => (s.id === stepId ? { ...s, status: "running" as const } : s)) + ) + onStepAction?.(stepId, action) + + try { + const res = await fetch("/api/ai/workflow-step", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + conversationId: "", + workflowId: proposal.id, + stepId, + action: "execute", + userRequest: proposal.userRequest, + step: { + title: step.title, + description: step.description, + recommendedTool: step.recommendedTool, + category: step.category, + neuralProvider: step.neuralProvider, + }, + }), + }) + + const result = (await res.json()) as WorkflowStepResult + + setSteps((prev) => + prev.map((s) => + s.id === stepId + ? { + ...s, + status: result.status, + error: result.error, + outputPath: result.outputPath, + } + : s + ) + ) + } catch (err) { + setSteps((prev) => + prev.map((s) => + s.id === stepId + ? { + ...s, + status: "failed" as const, + error: err instanceof Error ? err.message : "Step execution failed", + } + : s + ) + ) + } finally { + setExecutingStepId(null) + } + }, + [steps, proposal.id, proposal.userRequest, onStepAction] + ) + + const isStepBlocked = (step: WorkflowStep, index: number): boolean => { + if (!step.requiresPreviousStep || index === 0) return false + const prev = steps[index - 1] + return prev.status !== "completed" && prev.status !== "skipped" && prev.status !== "manual" + } + + return ( +
+ {/* Header */} +
+
+

{proposal.title}

+

+ {steps.length} steps β€’ Execute each step, do it manually, or skip +

+
+ + {proposal.strategy} + +
+ + {/* Progress bar */} +
+
+ {completedCount} / {steps.length} steps done + {progress}% +
+
+
+
+
+ + {/* Overall tips */} + {proposal.overallTips.length > 0 && ( +
+ + πŸ’‘ Workflow tips ({proposal.overallTips.length}) + +
    + {proposal.overallTips.map((tip, i) => ( +
  • {tip}
  • + ))} +
+
+ )} + + {/* Step cards */} +
+ {steps.map((step, index) => ( + handleStepAction(step.id, action)} + isExecuting={executingStepId === step.id} + /> + ))} +
+ + {/* Completion message */} + {progress === 100 && ( +
+ ✨ Workflow complete! +
+ )} +
+ ) +} diff --git a/data/blender-scripts/api_version_compatibility.py b/data/blender-scripts/api_version_compatibility.py index ac633af..1f984f9 100644 --- a/data/blender-scripts/api_version_compatibility.py +++ b/data/blender-scripts/api_version_compatibility.py @@ -267,6 +267,64 @@ def set_coat_safe(bsdf, weight=1.0, roughness=0.1): bsdf.inputs['Coat Weight'].default_value = weight bsdf.inputs['Coat Roughness'].default_value = roughness +# --- 19. Material blend_method / shadow_method Changes (5.0) --- +# REMOVED ATTRIBUTES (will crash if accessed): +# - mat.shadow_method β†’ DOES NOT EXIST in 5.x +# - mat.shadow_mode β†’ DOES NOT EXIST in 5.x +# +# VALID blend_method values (Blender 5.x): +# 'OPAQUE' β€” Default, fully opaque +# 'CLIP' β€” Binary alpha threshold +# 'HASHED' β€” Dithered alpha (good for foliage) +# 'BLEND' β€” True alpha blending (for glass, smoke, etc.) +# +# WRONG (will crash): mat.blend_method = 'ALPHA_BLEND' +# CORRECT: mat.blend_method = 'BLEND' +# +# For transparent materials: +# mat.blend_method = 'BLEND' +# # Do NOT set mat.shadow_method β€” it does not exist + + +# --- 20. EEVEE Removed Properties (5.0) --- +# The following SceneEEVEE properties are ALL REMOVED in Blender 5.x: +# - eevee.use_ssr β†’ Reflections are automatic +# - eevee.use_ssr_refraction β†’ Reflections are automatic +# - eevee.use_screen_space_reflections β†’ Reflections are automatic +# - eevee.use_gtao β†’ AO is automatic +# - eevee.use_bloom β†’ Use compositor Glare node +# - eevee.shadow_cascade_size β†’ Removed +# - eevee.taa_render_samples β†’ Use eevee.taa_samples instead +# +# For bloom in EEVEE 5.x, use the compositor: +# scene.use_nodes = True +# tree = scene.compositing_node_group +# glare = tree.nodes.new('CompositorNodeGlare') +# glare.glare_type = 'FOG_GLOW' +# glare.quality = 'HIGH' +# glare.size = 7 + + +# --- 21. Correct Transparent Material Setup (5.0) --- +def create_transparent_material(name, color=(0.8, 0.8, 0.8, 0.3)): + """ + Create a transparent material compatible with Blender 5.x. + + IMPORTANT: + - Use 'BLEND' not 'ALPHA_BLEND' for blend_method + - Do NOT set shadow_method or shadow_mode (removed in 5.x) + """ + mat = bpy.data.materials.new(name=name) + mat.use_nodes = True + mat.blend_method = 'BLEND' # NOT 'ALPHA_BLEND' + + bsdf = mat.node_tree.nodes.get("Principled BSDF") + if bsdf: + bsdf.inputs['Base Color'].default_value = color + bsdf.inputs['Alpha'].default_value = color[3] + + return mat + # ============================================================================= # STYLE CONVENTIONS (Official Blender Python Guidelines) diff --git a/data/blender-scripts/auto_retopology.py b/data/blender-scripts/auto_retopology.py new file mode 100644 index 0000000..feac39a --- /dev/null +++ b/data/blender-scripts/auto_retopology.py @@ -0,0 +1,316 @@ +""" +{ + "title": "Automated Retopology Pipeline", + "category": "topology", + "tags": ["retopology", "remesh", "quadriflow", "voxel", "cleanup", "topology", "mesh-repair"], + "description": "Production retopology pipeline for cleaning up sculpted or neural-generated meshes. Includes voxel remesh, Quadriflow quad-based remesh, decimation, mesh repair, and a full automated pipeline.", + "blender_version": "4.0+" +} +""" +import bpy +import bmesh + + +def voxel_remesh( + obj: bpy.types.Object, + voxel_size: float = 0.03, + adaptivity: float = 0.0 +) -> bpy.types.Object: + """ + Voxel remesh an object to create a unified, watertight mesh. + Ideal first step before Quadriflow β€” merges intersecting geometry and + removes internal faces. + + Args: + obj: Mesh object to remesh + voxel_size: Resolution (smaller = more detail). 0.003 for characters, 0.03 for environments + adaptivity: Reduce faces in flat areas (0.0 = uniform, 1.0 = maximum adaptive) + + Returns: + The remeshed object (same reference, modified in-place) + + Example: + >>> voxel_remesh(sculpted_head, voxel_size=0.005) + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + obj.data.remesh_voxel_size = voxel_size + obj.data.remesh_voxel_adaptivity = adaptivity + + bpy.ops.object.voxel_remesh() + + return obj + + +def quadriflow_remesh( + obj: bpy.types.Object, + target_faces: int = 5000, + use_symmetry: bool = True, + preserve_sharp: bool = True, + preserve_boundary: bool = True +) -> bpy.types.Object: + """ + Quadriflow remesh for clean quad-based topology. + Essential for animation deformation β€” produces all-quad meshes + with proper edge flow for subdivision and rigging. + + Args: + obj: Mesh object to remesh + target_faces: Desired number of faces in output mesh + use_symmetry: Enable X-axis symmetry detection + preserve_sharp: Keep sharp edges from original mesh + preserve_boundary: Keep boundary edges (open meshes) + + Returns: + The remeshed object (modified in-place) + + Example: + >>> quadriflow_remesh(character_body, target_faces=8000, use_symmetry=True) + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + bpy.ops.object.quadriflow_remesh( + target_faces=target_faces, + use_mesh_symmetry=use_symmetry, + preserve_sharp=preserve_sharp, + preserve_boundary=preserve_boundary + ) + + return obj + + +def decimate_mesh( + obj: bpy.types.Object, + ratio: float = None, + target_faces: int = None, + method: str = 'COLLAPSE' +) -> int: + """ + Reduce mesh density using the Decimate modifier. + + Args: + obj: Mesh object to decimate + ratio: Decimation ratio (0.0 to 1.0). Mutually exclusive with target_faces + target_faces: Target face count (calculates ratio automatically) + method: 'COLLAPSE' (edge collapse), 'UNSUBDIV' (un-subdivide), 'DISSOLVE' (planar dissolve) + + Returns: + Final face count after decimation + + Example: + >>> decimate_mesh(dense_mesh, ratio=0.5) + >>> decimate_mesh(dense_mesh, target_faces=2000) + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + current_faces = len(obj.data.polygons) + + if target_faces is not None: + ratio = min(1.0, max(0.01, target_faces / current_faces)) + elif ratio is None: + ratio = 0.5 + + mod = obj.modifiers.new(name="Decimate_Auto", type='DECIMATE') + mod.decimate_type = method + if method == 'COLLAPSE': + mod.ratio = ratio + elif method == 'UNSUBDIV': + mod.iterations = max(1, int(-1 * (ratio - 1) * 6)) + + bpy.ops.object.modifier_apply(modifier=mod.name) + + return len(obj.data.polygons) + + +def fix_mesh_issues( + obj: bpy.types.Object, + merge_distance: float = 0.0001, + fix_normals: bool = True, + remove_doubles: bool = True, + fill_holes: bool = True, + remove_loose: bool = True +) -> dict: + """ + Repair common mesh problems. Run this before retopology or export. + + Args: + obj: Mesh object to repair + merge_distance: Threshold for merging duplicate vertices + fix_normals: Recalculate normals to face outward + remove_doubles: Merge overlapping vertices + fill_holes: Fill small boundary holes + remove_loose: Remove disconnected vertices and edges + + Returns: + Dict with counts of issues fixed + + Example: + >>> report = fix_mesh_issues(imported_mesh) + >>> print(f"Fixed {report['doubles_removed']} doubles") + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + report = { + 'doubles_removed': 0, + 'normals_fixed': False, + 'holes_filled': 0, + 'loose_removed': 0 + } + + bpy.ops.object.mode_set(mode='EDIT') + + bm = bmesh.from_edit_mesh(obj.data) + + # Remove doubles (merge by distance) + if remove_doubles: + initial_verts = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=merge_distance) + report['doubles_removed'] = initial_verts - len(bm.verts) + + bmesh.update_edit_mesh(obj.data) + + # Remove loose geometry + if remove_loose: + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_loose() + count = obj.data.total_vert_sel + if count > 0: + bpy.ops.mesh.delete(type='VERT') + report['loose_removed'] = count + + # Fill holes + if fill_holes: + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_non_manifold(extend=False) + count = obj.data.total_edge_sel + if count > 0: + try: + bpy.ops.mesh.fill() + report['holes_filled'] = count + except RuntimeError: + pass # Fill can fail on complex boundaries + + # Fix normals + if fix_normals: + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.normals_make_consistent(inside=False) + report['normals_fixed'] = True + + bpy.ops.object.mode_set(mode='OBJECT') + + return report + + +def retopology_pipeline( + obj: bpy.types.Object, + target_faces: int = 5000, + voxel_size: float = 0.02, + use_symmetry: bool = True, + cleanup_first: bool = True +) -> dict: + """ + Full automated retopology pipeline. + Cleans mesh β†’ voxel unify β†’ optional decimate β†’ Quadriflow. + + This is the recommended way to retopologize neural-generated or sculpted meshes + for animation and game-dev use. + + Args: + obj: Mesh object to retopologize + target_faces: Desired final face count + voxel_size: Voxel remesh resolution (smaller = more detail preserved) + use_symmetry: Enable symmetry detection in Quadriflow + cleanup_first: Run mesh repair before retopology + + Returns: + Dict with pipeline results (face counts at each stage) + + Example: + >>> result = retopology_pipeline(neural_mesh, target_faces=8000) + >>> print(f"Reduced from {result['initial_faces']} to {result['final_faces']}") + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + result = { + 'initial_faces': len(obj.data.polygons), + 'after_cleanup': 0, + 'after_voxel': 0, + 'final_faces': 0 + } + + # Step 1: Cleanup + if cleanup_first: + fix_mesh_issues(obj) + result['after_cleanup'] = len(obj.data.polygons) + + # Step 2: Apply all transforms (required for consistent results) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + # Step 3: Voxel remesh to unify geometry + voxel_remesh(obj, voxel_size=voxel_size) + result['after_voxel'] = len(obj.data.polygons) + + # Step 4: If voxel result is much denser than target, pre-decimate + if len(obj.data.polygons) > target_faces * 3: + decimate_mesh(obj, target_faces=target_faces * 2) + + # Step 5: Quadriflow for clean quad topology + quadriflow_remesh( + obj, + target_faces=target_faces, + use_symmetry=use_symmetry, + preserve_sharp=True + ) + result['final_faces'] = len(obj.data.polygons) + + # Step 6: Smooth shading on final mesh + bpy.ops.object.shade_smooth() + + return result + + +def smooth_mesh( + obj: bpy.types.Object, + iterations: int = 2, + factor: float = 0.5 +) -> None: + """ + Apply Laplacian smooth to reduce noise while preserving shape. + + Args: + obj: Mesh object + iterations: Number of smoothing passes + factor: Smoothing strength (0.0 to 1.0) + + Example: + >>> smooth_mesh(retopoed_mesh, iterations=3, factor=0.3) + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + mod = obj.modifiers.new(name="Smooth_Auto", type='LAPLACIANSMOOTH') + mod.iterations = iterations + mod.lambda_factor = factor + + bpy.ops.object.modifier_apply(modifier=mod.name) diff --git a/data/blender-scripts/auto_rigify.py b/data/blender-scripts/auto_rigify.py new file mode 100644 index 0000000..2ed1672 --- /dev/null +++ b/data/blender-scripts/auto_rigify.py @@ -0,0 +1,353 @@ +""" +{ + "title": "Automated Rigify Rigging Pipeline", + "category": "rigging", + "tags": ["rigify", "rig", "metarig", "auto-rig", "weight-paint", "skeleton", "armature", "animation-ready"], + "description": "Automated character rigging using Blender's Rigify addon. Covers metarig creation from templates, alignment to mesh, rig generation, automatic weight painting, and Blender 4.x/5.0 compatibility.", + "blender_version": "4.0+" +} +""" +import bpy +import math + + +def ensure_rigify_enabled() -> bool: + """ + Ensure the Rigify addon is enabled. Rigify ships with Blender but + must be explicitly enabled before use. + + Returns: + True if Rigify is available + + Example: + >>> ensure_rigify_enabled() + True + """ + import addon_utils + loaded_default, loaded_state = addon_utils.check('rigify') + if not loaded_state: + bpy.ops.preferences.addon_enable(module='rigify') + return True + + +# --- Metarig Templates --- +METARIG_TEMPLATES = { + 'human': 'bpy.ops.object.armature_human_metarig_add', + 'basic_human': 'bpy.ops.object.armature_basic_human_metarig_add', + 'basic_quadruped':'bpy.ops.object.armature_basic_quadruped_metarig_add', + 'bird': 'bpy.ops.object.armature_bird_metarig_add', + 'cat': 'bpy.ops.object.armature_cat_metarig_add', + 'horse': 'bpy.ops.object.armature_horse_metarig_add', + 'shark': 'bpy.ops.object.armature_shark_metarig_add', + 'wolf': 'bpy.ops.object.armature_wolf_metarig_add', +} + + +def create_metarig( + template: str = 'human', + location: tuple = (0, 0, 0), + name: str = 'metarig' +) -> bpy.types.Object: + """ + Create a Rigify metarig from a built-in template. + + The metarig is a structural blueprint β€” it defines bone placement and + rig types. After positioning, call generate_rig() to produce the final rig. + + Args: + template: Template name. Options: 'human', 'basic_human', 'basic_quadruped', + 'bird', 'cat', 'horse', 'shark', 'wolf' + location: World-space position for the metarig + name: Name for the metarig object + + Returns: + The created metarig armature object + + Example: + >>> metarig = create_metarig('human', location=(0, 0, 0)) + >>> metarig = create_metarig('basic_quadruped', name='dog_metarig') + """ + ensure_rigify_enabled() + + # Deselect all + bpy.ops.object.select_all(action='DESELECT') + + template_lower = template.lower() + if template_lower not in METARIG_TEMPLATES: + raise ValueError( + f"Unknown template '{template}'. " + f"Available: {', '.join(METARIG_TEMPLATES.keys())}" + ) + + # Call the operator via eval (Rigify registers dynamic operators) + op_path = METARIG_TEMPLATES[template_lower] + eval(op_path + "()") + + metarig = bpy.context.active_object + metarig.name = name + metarig.location = location + + return metarig + + +def align_metarig_to_mesh( + metarig: bpy.types.Object, + target_mesh: bpy.types.Object, + align_feet_to_ground: bool = True +) -> None: + """ + Scale and position a metarig to match the bounding box of a target mesh. + This is the critical step before generating the rig β€” bones must be + inside the mesh for automatic weights to work. + + Args: + metarig: The metarig armature to align + target_mesh: The mesh to align to + align_feet_to_ground: Move metarig so feet bones sit at mesh's lowest point + + Example: + >>> align_metarig_to_mesh(metarig, character_mesh) + """ + # Get mesh bounding box in world space + mesh_bbox = [target_mesh.matrix_world @ bpy.mathutils.Vector(corner) + for corner in target_mesh.bound_box] + import mathutils + mesh_bbox = [target_mesh.matrix_world @ mathutils.Vector(corner) + for corner in target_mesh.bound_box] + + mesh_min = mathutils.Vector(( + min(v.x for v in mesh_bbox), + min(v.y for v in mesh_bbox), + min(v.z for v in mesh_bbox) + )) + mesh_max = mathutils.Vector(( + max(v.x for v in mesh_bbox), + max(v.y for v in mesh_bbox), + max(v.z for v in mesh_bbox) + )) + + mesh_height = mesh_max.z - mesh_min.z + mesh_center_xy = ((mesh_min.x + mesh_max.x) / 2, (mesh_min.y + mesh_max.y) / 2) + + # Get metarig height + meta_bbox = [metarig.matrix_world @ mathutils.Vector(corner) + for corner in metarig.bound_box] + meta_height = max(v.z for v in meta_bbox) - min(v.z for v in meta_bbox) + + if meta_height > 0: + scale_factor = mesh_height / meta_height + metarig.scale = (scale_factor, scale_factor, scale_factor) + + # Center metarig on mesh XY + metarig.location.x = mesh_center_xy[0] + metarig.location.y = mesh_center_xy[1] + + if align_feet_to_ground: + metarig.location.z = mesh_min.z + + # Apply scale so rig generation uses correct dimensions + bpy.context.view_layer.objects.active = metarig + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + + +def generate_rig( + metarig: bpy.types.Object, + rig_name: str = None +) -> bpy.types.Object: + """ + Generate the final control rig from a positioned metarig. + This calls Rigify's core generation which creates the full rig with: + - DEF- bones (deformation β€” bind mesh to these) + - MCH- bones (mechanism β€” intermediate helpers) + - ORG- bones (original positions) + - Control bones (what animators manipulate) + + Args: + metarig: The aligned metarig armature + rig_name: Optional name for the generated rig (default: 'rig') + + Returns: + The generated rig armature object + + Example: + >>> rig = generate_rig(metarig, rig_name='Character_Rig') + """ + ensure_rigify_enabled() + import rigify.generate + + bpy.context.view_layer.objects.active = metarig + metarig.select_set(True) + + # Generate the rig + rigify.generate.generate_rig(bpy.context, metarig) + + # The generated rig becomes the active object + rig = bpy.context.active_object + + if rig_name: + rig.name = rig_name + + return rig + + +def bind_mesh_to_rig( + mesh: bpy.types.Object, + rig: bpy.types.Object, + method: str = 'AUTO', + heat_diffusion: bool = True +) -> None: + """ + Bind a mesh to a rig using automatic weight painting. + + Args: + mesh: The mesh object to bind + rig: The armature/rig object + method: Binding method: + - 'AUTO': Automatic weights (best for organic characters) + - 'ENVELOPE': Envelope-based weights (rough, fast) + - 'EMPTY': Create empty vertex groups (for manual weight painting) + - 'NAME': Match vertex groups to bones by name (for pre-weighted meshes) + heat_diffusion: Use heat diffusion for AUTO method (better quality, slower) + + Example: + >>> bind_mesh_to_rig(character_mesh, rig, method='AUTO') + """ + # Deselect all + bpy.ops.object.select_all(action='DESELECT') + + # Select mesh first, then rig (order matters!) + mesh.select_set(True) + rig.select_set(True) + bpy.context.view_layer.objects.active = rig + + method_map = { + 'AUTO': 'ARMATURE_AUTO', + 'ENVELOPE': 'ARMATURE_ENVELOPE', + 'EMPTY': 'ARMATURE', + 'NAME': 'ARMATURE_NAME' + } + + parent_type = method_map.get(method.upper(), 'ARMATURE_AUTO') + bpy.ops.object.parent_set(type=parent_type) + + +def set_bone_rigify_type( + metarig: bpy.types.Object, + bone_name: str, + rigify_type: str +) -> None: + """ + Set the Rigify rig type for a specific bone in the metarig. + + Common rig types: + - 'spines.basic_spine': Spine chain + - 'spines.basic_tail': Tail chain + - 'limbs.arm': Arm with IK/FK + - 'limbs.leg': Leg with IK/FK + - 'limbs.paw': Animal paw + - 'faces.super_face': Full facial rig + - 'basic.super_copy': Simple copy transforms + + Args: + metarig: The metarig armature + bone_name: Name of the bone to configure + rigify_type: Rigify rig type identifier + + Example: + >>> set_bone_rigify_type(metarig, 'spine.003', 'spines.basic_tail') + """ + bpy.context.view_layer.objects.active = metarig + bpy.ops.object.mode_set(mode='POSE') + + pose_bone = metarig.pose.bones.get(bone_name) + if not pose_bone: + raise ValueError(f"Bone '{bone_name}' not found in metarig") + + pose_bone.rigify_type = rigify_type + + bpy.ops.object.mode_set(mode='OBJECT') + + +def rigify_pipeline( + target_mesh: bpy.types.Object, + template: str = 'human', + rig_name: str = 'Character_Rig', + auto_align: bool = True +) -> dict: + """ + Full automated Rigify rigging pipeline. + Creates metarig β†’ aligns to mesh β†’ generates rig β†’ binds mesh. + + This is the recommended high-level function for automatic rigging. + + Args: + target_mesh: The mesh to rig + template: Metarig template (see create_metarig for options) + rig_name: Name for the final rig + auto_align: Automatically scale/position metarig to mesh + + Returns: + Dict with 'metarig', 'rig', and 'mesh' object references + + Example: + >>> result = rigify_pipeline(character_mesh, template='human') + >>> rig = result['rig'] + """ + ensure_rigify_enabled() + + result = {} + + # Step 1: Create metarig + metarig = create_metarig( + template=template, + location=target_mesh.location, + name=f'{rig_name}_metarig' + ) + result['metarig'] = metarig + + # Step 2: Align to target mesh + if auto_align: + align_metarig_to_mesh(metarig, target_mesh) + + # Step 3: Generate the rig + rig = generate_rig(metarig, rig_name=rig_name) + result['rig'] = rig + + # Step 4: Bind mesh to rig + bind_mesh_to_rig(target_mesh, rig, method='AUTO') + result['mesh'] = target_mesh + + # Step 5: Hide metarig (no longer needed for animation) + metarig.hide_set(True) + + return result + + +def assign_bone_weight( + mesh: bpy.types.Object, + bone_name: str, + vertex_indices: list, + weight: float = 1.0, + mode: str = 'REPLACE' +) -> None: + """ + Manually assign vertex weights to a bone's vertex group. + Useful for rigid parts (helmets, armor, accessories) that need + 100% weight to a single bone. + + Args: + mesh: The mesh object + bone_name: Name of the bone (vertex group) + vertex_indices: List of vertex indices to assign + weight: Weight value (0.0 to 1.0) + mode: 'REPLACE', 'ADD', or 'SUBTRACT' + + Example: + >>> # Assign helmet vertices fully to head bone + >>> assign_bone_weight(character, 'DEF-spine.006', [100, 101, 102], weight=1.0) + """ + vgroup = mesh.vertex_groups.get(bone_name) + if not vgroup: + vgroup = mesh.vertex_groups.new(name=bone_name) + + vgroup.add(vertex_indices, weight, mode) diff --git a/data/blender-scripts/auto_uv_unwrap.py b/data/blender-scripts/auto_uv_unwrap.py new file mode 100644 index 0000000..40c3a53 --- /dev/null +++ b/data/blender-scripts/auto_uv_unwrap.py @@ -0,0 +1,362 @@ +""" +{ + "title": "Automated UV Unwrap Pipeline", + "category": "uv", + "tags": ["uv", "unwrap", "lightmap", "texel-density", "baking", "pipeline", "auto-uv"], + "description": "High-level UV unwrapping pipeline that auto-detects the best projection method per object, creates lightmap UVs, checks texel density, and handles batch UV operations for production workflows.", + "blender_version": "4.0+" +} +""" +import bpy +import bmesh +import math + + +def auto_uv_pipeline( + obj: bpy.types.Object, + island_margin: float = 0.02, + normalize: bool = True, + pack: bool = True +) -> dict: + """ + Fully automated UV unwrap pipeline. + Analyzes object shape to pick the best UV projection method, + unwraps, packs islands, and normalizes texel density. + + Shape detection logic: + - Flat / planar β†’ Project From View + - Box-like (roughly equal dims) β†’ Cube Projection + - Tall / long β†’ Cylinder Projection + - Spherical β†’ Sphere Projection + - Complex / organic β†’ Smart UV Project with auto-seams + + Args: + obj: Mesh object to unwrap + island_margin: Space between UV islands + normalize: Equalize texel density across all islands + pack: Pack UV islands to fill 0-1 space + + Returns: + Dict with 'method' used and 'island_count' + + Example: + >>> result = auto_uv_pipeline(character_mesh) + >>> print(f"Used {result['method']} β€” {result['island_count']} islands") + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + # Ensure UVs exist + if not obj.data.uv_layers: + obj.data.uv_layers.new(name="UVMap") + + # Analyze bounding box to pick method + dims = obj.dimensions + max_dim = max(dims) + min_dim = min(dims) if min(dims) > 0 else 0.001 + + aspect_ratio = max_dim / min_dim + + if aspect_ratio > 5: + method = 'cylinder' + elif aspect_ratio < 1.3 and abs(dims.x - dims.y) < 0.1 * max_dim and abs(dims.y - dims.z) < 0.1 * max_dim: + method = 'sphere' + elif aspect_ratio < 2.0 and min_dim / max_dim > 0.5: + method = 'cube' + else: + method = 'smart' + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + if method == 'smart': + bpy.ops.uv.smart_project( + angle_limit=math.radians(66), + island_margin=island_margin, + scale_to_bounds=True + ) + elif method == 'cube': + bpy.ops.uv.cube_project( + cube_size=max_dim, + correct_aspect=True + ) + elif method == 'cylinder': + bpy.ops.uv.cylinder_project( + direction='ALIGN_TO_OBJECT', + radius=max_dim / 2 + ) + elif method == 'sphere': + bpy.ops.uv.sphere_project(direction='ALIGN_TO_OBJECT') + + # Normalize texel density + if normalize: + bpy.ops.uv.average_islands_scale() + + # Pack islands + if pack: + bpy.ops.uv.pack_islands(margin=island_margin, rotate=True) + + bpy.ops.object.mode_set(mode='OBJECT') + + # Count UV islands + island_count = _count_uv_islands(obj) + + return { + 'method': method, + 'island_count': island_count, + 'object': obj.name + } + + +def lightmap_uv( + obj: bpy.types.Object, + uv_name: str = "Lightmap", + margin: float = 0.03 +) -> None: + """ + Create a second UV channel optimized for lightmap baking. + Uses Smart UV Project with high margin for clean lightmap padding. + + Args: + obj: Mesh object + uv_name: Name for the lightmap UV layer + margin: Island margin (higher than normal for lightmap bleeding) + + Example: + >>> lightmap_uv(building_mesh, margin=0.05) + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + # Create new UV layer + uv_layer = obj.data.uv_layers.new(name=uv_name) + obj.data.uv_layers.active = uv_layer + + # Smart UV project is best for lightmaps (non-overlapping) + 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=margin, + scale_to_bounds=True + ) + bpy.ops.uv.pack_islands(margin=margin, rotate=True) + bpy.ops.object.mode_set(mode='OBJECT') + + +def uv_density_check( + obj: bpy.types.Object, + texture_size: int = 1024 +) -> dict: + """ + Check texel density across UV islands for quality assurance. + Reports min/max/average texels per unit for identifying + under-textured or over-textured areas. + + Args: + obj: Mesh object with UVs to check + texture_size: Expected texture resolution in pixels + + Returns: + Dict with 'min_density', 'max_density', 'avg_density', 'variance' + + Example: + >>> report = uv_density_check(game_asset, texture_size=2048) + >>> if report['variance'] > 2.0: + ... print("Warning: uneven texel density") + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + mesh = obj.data + if not mesh.uv_layers.active: + return {'min_density': 0, 'max_density': 0, 'avg_density': 0, 'variance': 0} + + uv_layer = mesh.uv_layers.active.data + densities = [] + + for poly in mesh.polygons: + # Calculate 3D face area + face_area_3d = poly.area + if face_area_3d < 1e-8: + continue + + # Calculate UV face area + uv_coords = [] + for loop_idx in poly.loop_indices: + uv_coords.append(uv_layer[loop_idx].uv) + + if len(uv_coords) >= 3: + uv_area = _polygon_area_2d(uv_coords) + if uv_area > 1e-8: + density = (uv_area * texture_size * texture_size) / face_area_3d + densities.append(density) + + if not densities: + return {'min_density': 0, 'max_density': 0, 'avg_density': 0, 'variance': 0} + + avg = sum(densities) / len(densities) + variance = max(densities) / max(min(densities), 0.001) + + return { + 'min_density': min(densities), + 'max_density': max(densities), + 'avg_density': avg, + 'variance': variance + } + + +def multi_object_uv( + objects: list = None, + method: str = 'smart', + island_margin: float = 0.02 +) -> list: + """ + Batch UV unwrap multiple objects with consistent settings. + If no objects are specified, uses all selected mesh objects. + + Args: + objects: List of mesh objects (or None for selected) + method: 'smart', 'cube', 'cylinder', 'sphere', or 'auto' + island_margin: Space between islands + + Returns: + List of dicts with results per object + + Example: + >>> results = multi_object_uv(method='auto') + """ + if objects is None: + objects = [o for o in bpy.context.selected_objects if o.type == 'MESH'] + + results = [] + for obj in objects: + if obj.type != 'MESH': + continue + if method == 'auto': + result = auto_uv_pipeline(obj, island_margin=island_margin) + else: + result = _unwrap_single(obj, method, island_margin) + results.append(result) + + return results + + +def create_uv_for_texture_baking( + obj: bpy.types.Object, + uv_name: str = "BakeUV", + margin: float = 0.04 +) -> None: + """ + Create UV layout optimized for texture baking. + Non-overlapping, maximum space usage, higher margins to prevent bleeding. + + Args: + obj: Mesh object + uv_name: Name for the baking UV layer + margin: Island margin (larger for bake bleed prevention) + + Example: + >>> create_uv_for_texture_baking(high_poly_model) + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + # Create dedicated bake UV layer + uv_layer = obj.data.uv_layers.new(name=uv_name) + obj.data.uv_layers.active = uv_layer + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + # Use angle-based unwrap with auto-seams for best bake coverage + bpy.ops.mesh.mark_seam(clear=True) + bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(40)) + bpy.ops.mesh.mark_seam() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=margin) + + # Normalize and pack + bpy.ops.uv.average_islands_scale() + bpy.ops.uv.pack_islands(margin=margin, rotate=True) + + bpy.ops.object.mode_set(mode='OBJECT') + + +# --- Internal Helpers --- + +def _count_uv_islands(obj: bpy.types.Object) -> int: + """Count the number of UV islands in the active UV layer.""" + bm = bmesh.new() + bm.from_mesh(obj.data) + uv_layer = bm.loops.layers.uv.active + if not uv_layer: + bm.free() + return 0 + + # Simple island counting via face connectivity in UV space + visited = set() + islands = 0 + for face in bm.faces: + if face.index not in visited: + # BFS through UV-connected faces + queue = [face] + while queue: + f = queue.pop() + if f.index in visited: + continue + visited.add(f.index) + for edge in f.edges: + for linked_face in edge.link_faces: + if linked_face.index not in visited: + queue.append(linked_face) + islands += 1 + + bm.free() + return islands + + +def _polygon_area_2d(coords: list) -> float: + """Calculate area of a 2D polygon using the shoelace formula.""" + n = len(coords) + area = 0.0 + for i in range(n): + j = (i + 1) % n + area += coords[i][0] * coords[j][1] + area -= coords[j][0] * coords[i][1] + return abs(area) / 2.0 + + +def _unwrap_single(obj, method, margin): + """Unwrap a single object with the specified method.""" + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + if not obj.data.uv_layers: + obj.data.uv_layers.new(name="UVMap") + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + if method == 'smart': + bpy.ops.uv.smart_project(angle_limit=math.radians(66), island_margin=margin) + elif method == 'cube': + bpy.ops.uv.cube_project(cube_size=max(obj.dimensions)) + elif method == 'cylinder': + bpy.ops.uv.cylinder_project() + elif method == 'sphere': + bpy.ops.uv.sphere_project() + + bpy.ops.uv.pack_islands(margin=margin, rotate=True) + bpy.ops.object.mode_set(mode='OBJECT') + + return {'method': method, 'object': obj.name, 'island_count': _count_uv_islands(obj)} diff --git a/data/blender-scripts/blender_api_pitfalls.py b/data/blender-scripts/blender_api_pitfalls.py index dcc894a..9559b81 100644 --- a/data/blender-scripts/blender_api_pitfalls.py +++ b/data/blender-scripts/blender_api_pitfalls.py @@ -277,3 +277,48 @@ def apply_modifier_CORRECT(obj, modifier_name): """CORRECT β€” Use temp_override for operator context.""" with bpy.context.temp_override(object=obj): bpy.ops.object.modifier_apply(modifier=modifier_name) + + +# ============================================================================= +# PITFALL 16: bpy.data.textures vs Shader Node Inputs +# ============================================================================= +# Problem: bpy.data.textures (modifier textures) use PROPERTIES like .noise_scale. +# Shader nodes (ShaderNodeTexNoise etc.) use INPUTS like inputs['Scale']. +# These are TWO COMPLETELY DIFFERENT APIs for textures. +# Error: AttributeError: 'NoiseTexture' object has no attribute 'noise_scale' + +def set_noise_scale_WRONG(noise_node): + """INCORRECT β€” noise_scale is a bpy.data.textures property, not a shader node.""" + noise_node.noise_scale = 5.0 # AttributeError! + +def set_noise_scale_CORRECT(noise_node): + """CORRECT β€” Shader nodes use inputs['Scale'].default_value.""" + noise_node.inputs['Scale'].default_value = 5.0 # Correct for shader nodes + +def set_modifier_texture_scale_CORRECT(): + """CORRECT β€” bpy.data.textures use .noise_scale property directly.""" + tex = bpy.data.textures.new(name="MyNoise", type='NOISE') + tex.noise_scale = 0.5 # Correct for bpy.data.textures (modifier textures) + + +# ============================================================================= +# PITFALL 17: Camera X Rotation Direction +# ============================================================================= +# Problem: Camera Euler X rotation is often set incorrectly when aiming up or down. +# X=90Β° (radians: pi/2) = perfectly horizontal, looking straight ahead. +# X < 90Β° = looking DOWN. X > 90Β° = looking UP. +# Solution: Use math.radians() and remember: >90Β° = up, <90Β° = down. + +import math + +def set_camera_looking_slightly_up(camera_obj): + """CORRECT β€” X=95Β° means looking 5Β° above horizontal.""" + camera_obj.rotation_euler = (math.radians(95), 0, 0) + +def set_camera_looking_slightly_down(camera_obj): + """CORRECT β€” X=80Β° means looking 10Β° below horizontal.""" + camera_obj.rotation_euler = (math.radians(80), 0, 0) + +def set_camera_looking_horizontal(camera_obj): + """CORRECT β€” X=90Β° = perfectly horizontal.""" + camera_obj.rotation_euler = (math.radians(90), 0, 0) diff --git a/data/blender-scripts/displacement_textures.py b/data/blender-scripts/displacement_textures.py new file mode 100644 index 0000000..d0b5222 --- /dev/null +++ b/data/blender-scripts/displacement_textures.py @@ -0,0 +1,310 @@ +""" +Displacement & Surface Texture Recipes β€” Blender 4.0/5.0 +========================================================= +Category: materials +Blender: 4.0+ / 5.0 +Source: Blender API Docs + Stack Exchange best practices + +Procedural displacement patterns for terrain, raked sand, water ripples, +rocky surfaces, and other surface deformations using shader nodes. + +TWO METHODS FOR SURFACE DEFORMATION: +1. Shader Displacement (node-based, no mesh change, render-only in Cycles) + - ShaderNodeDisplacement β†’ Material Output 'Displacement' input + - Requires: material.displacement_method = 'DISPLACEMENT' or 'BOTH' + - Works in Cycles only (EEVEE uses bump approximation) + +2. Modifier Displacement (mesh-level, works everywhere) + - Subdivision Surface modifier + Displace modifier + - Uses bpy.data.textures for pattern source + - Visible in viewport and all render engines + +SHADER NODE REFERENCE: +| Node Type | Key Properties | +|------------------------------|-----------------------------------------| +| ShaderNodeTexWave | wave_type: BANDS/RINGS | +| | wave_profile: SIN/SAW/TRI | +| | bands_direction: X/Y/Z/DIAGONAL | +| | Inputs: Scale, Distortion, Detail | +| ShaderNodeTexNoise | Inputs: Scale, Detail, Roughness | +| ShaderNodeDisplacement | space: OBJECT/WORLD | +| | Inputs: Height, Midlevel, Scale, Normal | +| ShaderNodeTexMusgrave | REMOVED in Blender 4.1+ | +| | Use ShaderNodeTexNoise instead | +""" + +import bpy + + +# ============================================================================= +# SHADER-BASED DISPLACEMENT (Cycles) +# ============================================================================= + +def create_raked_sand_material(name="RakedSand", scale=8.0, distortion=2.0): + """Create raked sand/zen garden material with wave displacement. + + Uses ShaderNodeTexWave (BANDS, SIN profile) for parallel rake lines, + mixed with subtle noise for natural variation. + Connects to Material Output Displacement socket for true displacement. + """ + mat = bpy.data.materials.new(name=name) + mat.displacement_method = 'BOTH' # Bump + true displacement + nodes = mat.node_tree.nodes + links = mat.node_tree.links + nodes.clear() + + # Output + Principled + output = nodes.new('ShaderNodeOutputMaterial') + output.location = (600, 0) + bsdf = nodes.new('ShaderNodeBsdfPrincipled') + bsdf.location = (200, 0) + bsdf.inputs['Base Color'].default_value = (0.76, 0.70, 0.58, 1.0) + bsdf.inputs['Roughness'].default_value = 0.85 + links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) + + # Texture Coordinates + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-800, 0) + + # Wave Texture β€” parallel rake lines + wave = nodes.new('ShaderNodeTexWave') + wave.location = (-400, 100) + wave.wave_type = 'BANDS' + wave.wave_profile = 'SIN' + wave.bands_direction = 'X' + wave.inputs['Scale'].default_value = scale + wave.inputs['Distortion'].default_value = distortion + wave.inputs['Detail'].default_value = 3.0 + wave.inputs['Detail Scale'].default_value = 1.5 + + # Noise for natural variation + noise = nodes.new('ShaderNodeTexNoise') + noise.location = (-400, -100) + noise.inputs['Scale'].default_value = 25.0 + noise.inputs['Detail'].default_value = 4.0 + noise.inputs['Roughness'].default_value = 0.6 + + # Mix wave + noise + mix = nodes.new('ShaderNodeMix') + mix.location = (-200, 0) + mix.data_type = 'FLOAT' + mix.inputs[0].default_value = 0.7 # Factor: mostly wave + links.new(wave.outputs['Fac'], mix.inputs[2]) # A + links.new(noise.outputs['Fac'], mix.inputs[3]) # B + + # Displacement node + disp = nodes.new('ShaderNodeDisplacement') + disp.location = (200, -200) + disp.inputs['Scale'].default_value = 0.05 + disp.inputs['Midlevel'].default_value = 0.5 + + links.new(tex_coord.outputs['Object'], wave.inputs['Vector']) + links.new(tex_coord.outputs['Object'], noise.inputs['Vector']) + links.new(mix.outputs[0], disp.inputs['Height']) + links.new(disp.outputs['Displacement'], output.inputs['Displacement']) + + return mat + + +def create_water_ripple_material(name="WaterRipple"): + """Create concentric water ripple displacement using RINGS wave type.""" + mat = bpy.data.materials.new(name=name) + mat.displacement_method = 'BOTH' + nodes = mat.node_tree.nodes + links = mat.node_tree.links + nodes.clear() + + output = nodes.new('ShaderNodeOutputMaterial') + output.location = (600, 0) + bsdf = nodes.new('ShaderNodeBsdfPrincipled') + bsdf.location = (200, 0) + bsdf.inputs['Base Color'].default_value = (0.1, 0.3, 0.5, 1.0) + bsdf.inputs['Roughness'].default_value = 0.05 + bsdf.inputs['Transmission Weight'].default_value = 0.6 + bsdf.inputs['IOR'].default_value = 1.33 + links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) + + wave = nodes.new('ShaderNodeTexWave') + wave.location = (-400, 0) + wave.wave_type = 'RINGS' + wave.wave_profile = 'SIN' + wave.inputs['Scale'].default_value = 12.0 + wave.inputs['Distortion'].default_value = 1.0 + wave.inputs['Detail'].default_value = 2.0 + + disp = nodes.new('ShaderNodeDisplacement') + disp.location = (200, -200) + disp.inputs['Scale'].default_value = 0.03 + + links.new(wave.outputs['Fac'], disp.inputs['Height']) + links.new(disp.outputs['Displacement'], output.inputs['Displacement']) + + return mat + + +def create_rocky_surface_material(name="RockySurface"): + """Create rough rocky terrain using noise displacement + bump.""" + mat = bpy.data.materials.new(name=name) + mat.displacement_method = 'BOTH' + nodes = mat.node_tree.nodes + links = mat.node_tree.links + nodes.clear() + + output = nodes.new('ShaderNodeOutputMaterial') + output.location = (600, 0) + bsdf = nodes.new('ShaderNodeBsdfPrincipled') + bsdf.location = (200, 0) + bsdf.inputs['Base Color'].default_value = (0.35, 0.30, 0.25, 1.0) + bsdf.inputs['Roughness'].default_value = 0.9 + links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) + + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-800, 0) + + # Large-scale noise for terrain shape + noise_large = nodes.new('ShaderNodeTexNoise') + noise_large.location = (-400, 100) + noise_large.inputs['Scale'].default_value = 3.0 + noise_large.inputs['Detail'].default_value = 8.0 + noise_large.inputs['Roughness'].default_value = 0.7 + + # Small-scale noise for surface detail + noise_small = nodes.new('ShaderNodeTexNoise') + noise_small.location = (-400, -100) + noise_small.inputs['Scale'].default_value = 20.0 + noise_small.inputs['Detail'].default_value = 12.0 + noise_small.inputs['Roughness'].default_value = 0.8 + + # Mix large + small + mix = nodes.new('ShaderNodeMix') + mix.location = (-200, 0) + mix.data_type = 'FLOAT' + mix.inputs[0].default_value = 0.3 + links.new(noise_large.outputs['Fac'], mix.inputs[2]) + links.new(noise_small.outputs['Fac'], mix.inputs[3]) + + disp = nodes.new('ShaderNodeDisplacement') + disp.location = (200, -200) + disp.inputs['Scale'].default_value = 0.15 + + links.new(tex_coord.outputs['Object'], noise_large.inputs['Vector']) + links.new(tex_coord.outputs['Object'], noise_small.inputs['Vector']) + links.new(mix.outputs[0], disp.inputs['Height']) + links.new(disp.outputs['Displacement'], output.inputs['Displacement']) + + return mat + + +# ============================================================================= +# MODIFIER-BASED DISPLACEMENT (works in EEVEE + viewport) +# ============================================================================= + +def add_wave_displacement_modifier(obj, texture_type='WAVES', + scale=0.1, subdivisions=4): + """Add modifier-based displacement using Subdivision + Displace. + + This method works in EEVEE and is visible in the viewport. + Good for: terrain, raked sand, rippled surfaces. + + Args: + obj: Target mesh object + texture_type: 'WAVES', 'NOISE', 'VORONOI', 'CLOUDS' + scale: Displacement strength + subdivisions: Subdivision level (higher = smoother) + """ + # Add subdivision for geometry detail + subsurf = obj.modifiers.new(name="Subdiv", type='SUBSURF') + subsurf.levels = subdivisions + subsurf.render_levels = subdivisions + 1 + + # Create displacement texture + tex = bpy.data.textures.new(name=f"{obj.name}_DispTex", type=texture_type) + + if texture_type == 'WAVES': + # Blender internal texture for modifier displacement + # Note: This uses bpy.data.textures, NOT shader nodes + pass # WAVES type has built-in wave pattern + elif texture_type == 'NOISE': + tex.noise_scale = 0.5 + elif texture_type == 'VORONOI': + tex.noise_intensity = 1.0 + elif texture_type == 'CLOUDS': + tex.noise_scale = 0.5 + tex.noise_depth = 3 + + # Add displace modifier + displace = obj.modifiers.new(name="Displace", type='DISPLACE') + displace.texture = tex + displace.strength = scale + displace.mid_level = 0.5 + + return subsurf, displace + + +def create_terrain_plane(name="Terrain", size=10, subdivisions=5, + noise_scale=0.3): + """Create a subdivided plane with noise displacement for terrain. + + Creates a ready-to-use terrain mesh with modifier-based displacement. + Works in both EEVEE and Cycles, visible in viewport. + """ + # Create subdivided plane + bpy.ops.mesh.primitive_plane_add(size=size, location=(0, 0, 0)) + terrain = bpy.context.active_object + terrain.name = name + + # Add subdivision + displacement + add_wave_displacement_modifier( + terrain, + texture_type='NOISE', + scale=noise_scale, + subdivisions=subdivisions + ) + + # Add earth-tone material + mat = bpy.data.materials.new(name=f"{name}_Mat") + bsdf = mat.node_tree.nodes.get("Principled BSDF") + bsdf.inputs['Base Color'].default_value = (0.4, 0.32, 0.2, 1.0) + bsdf.inputs['Roughness'].default_value = 0.9 + terrain.data.materials.append(mat) + + return terrain + + +def create_raked_sand_plane(name="RakedSandFloor", size=10): + """Create a zen garden raked sand floor plane. + + Uses modifier-based displacement visible in EEVEE and viewport. + Combines wave pattern (rake lines) with subtle noise (natural grain). + """ + bpy.ops.mesh.primitive_plane_add(size=size, location=(0, 0, 0)) + sand = bpy.context.active_object + sand.name = name + + # Subdivision for detail + subsurf = sand.modifiers.new(name="Subdiv", type='SUBSURF') + subsurf.levels = 5 + subsurf.render_levels = 6 + + # Wave displacement for rake lines + wave_tex = bpy.data.textures.new(name="RakeWaves", type='WAVES') + wave_disp = sand.modifiers.new(name="RakeLines", type='DISPLACE') + wave_disp.texture = wave_tex + wave_disp.strength = 0.04 + wave_disp.mid_level = 0.5 + + # Subtle noise for natural grain + noise_tex = bpy.data.textures.new(name="SandGrain", type='NOISE') + noise_disp = sand.modifiers.new(name="SandGrain", type='DISPLACE') + noise_disp.texture = noise_tex + noise_disp.strength = 0.01 + noise_disp.mid_level = 0.5 + + # Sand material + mat = bpy.data.materials.new(name="SandMaterial") + bsdf = mat.node_tree.nodes.get("Principled BSDF") + bsdf.inputs['Base Color'].default_value = (0.76, 0.70, 0.58, 1.0) + bsdf.inputs['Roughness'].default_value = 0.85 + sand.data.materials.append(mat) + + return sand diff --git a/data/blender-scripts/hdri_lighting.py b/data/blender-scripts/hdri_lighting.py new file mode 100644 index 0000000..5414f60 --- /dev/null +++ b/data/blender-scripts/hdri_lighting.py @@ -0,0 +1,156 @@ +""" +HDRI Environment & Image-Based Lighting β€” Blender 4.0/5.0 +========================================================== +Category: lighting +Blender: 4.0+ / 5.0 +Source: Blender Stack Exchange + Official Docs + +HDRI (High Dynamic Range Image) environment textures for realistic +image-based lighting. Covers: +- Loading HDRI files as world environment +- Background strength + color control +- Rotation via Mapping node +- Ground projection for infinite backgrounds +- Combining HDRI with solid background (compositing trick) + +NODE PATTERN FOR HDRI: + Tex Coord β†’ Mapping β†’ Environment Texture β†’ Background β†’ World Output + ↑ + Strength value + +IMPORTANT NOTES: +- Environment Texture node: type = 'ShaderNodeTexEnvironment' +- NOT ShaderNodeTexImage β€” that's for meshes, not world backgrounds +- HDRI files typically use .hdr or .exr format +- Color space should usually be 'Linear' (default for HDR files) +- For non-HDR images used as background: set to 'sRGB' +""" + +import bpy +import math + + +def setup_hdri_world(hdri_path, strength=1.0, rotation_z=0.0): + """Set up HDRI environment lighting for the scene. + + This is the standard pattern for image-based lighting in Blender. + Creates: Tex Coord β†’ Mapping β†’ Environment Texture β†’ Background β†’ Output + + Args: + hdri_path: Path to .hdr/.exr file (absolute or relative //) + strength: Background light intensity (1.0 = natural, 2.0 = bright) + rotation_z: Z rotation in degrees for rotating the environment + """ + world = bpy.context.scene.world + if world is None: + world = bpy.data.worlds.new("World") + bpy.context.scene.world = world + + node_tree = world.node_tree + nodes = node_tree.nodes + links = node_tree.links + nodes.clear() + + # Texture Coordinate β†’ Mapping (for rotation control) + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-600, 0) + + mapping = nodes.new('ShaderNodeMapping') + mapping.location = (-400, 0) + mapping.inputs['Rotation'].default_value[2] = math.radians(rotation_z) + + # Environment Texture β€” NOT ShaderNodeTexImage! + env_tex = nodes.new('ShaderNodeTexEnvironment') + env_tex.location = (-200, 0) + env_tex.image = bpy.data.images.load(hdri_path) + # HDR files default to Linear color space β€” do NOT change to Non-Color + + # Background shader + background = nodes.new('ShaderNodeBackground') + background.location = (100, 0) + background.inputs['Strength'].default_value = strength + + # World Output + output = nodes.new('ShaderNodeOutputWorld') + output.location = (300, 0) + + # Link chain + links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector']) + links.new(mapping.outputs['Vector'], env_tex.inputs['Vector']) + links.new(env_tex.outputs['Color'], background.inputs['Color']) + links.new(background.outputs['Background'], output.inputs['Surface']) + + return world + + +def setup_solid_color_world(color=(0.05, 0.05, 0.1), strength=1.0): + """Set up a solid color world background (no HDRI needed). + + Useful for studio-style renders or when you want full lighting control. + """ + world = bpy.context.scene.world + if world is None: + world = bpy.data.worlds.new("World") + bpy.context.scene.world = world + + node_tree = world.node_tree + nodes = node_tree.nodes + links = node_tree.links + nodes.clear() + + background = nodes.new('ShaderNodeBackground') + background.location = (0, 0) + background.inputs['Color'].default_value = (*color, 1.0) + background.inputs['Strength'].default_value = strength + + output = nodes.new('ShaderNodeOutputWorld') + output.location = (200, 0) + + links.new(background.outputs['Background'], output.inputs['Surface']) + return world + + +def setup_gradient_sky_world(sky_color=(0.4, 0.6, 0.9), + horizon_color=(0.85, 0.85, 0.9), + strength=1.0): + """Create a gradient sky from horizon to zenith (no HDRI file needed). + + Uses ColorRamp + Texture Coordinate to create a natural sky gradient. + Good for outdoor scenes without a specific HDRI. + """ + world = bpy.context.scene.world + if world is None: + world = bpy.data.worlds.new("World") + bpy.context.scene.world = world + + node_tree = world.node_tree + nodes = node_tree.nodes + links = node_tree.links + nodes.clear() + + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-600, 0) + + separate = nodes.new('ShaderNodeSeparateXYZ') + separate.location = (-400, 0) + + ramp = nodes.new('ShaderNodeValToRGB') + ramp.location = (-200, 0) + ramp.color_ramp.elements[0].position = 0.4 + ramp.color_ramp.elements[0].color = (*horizon_color, 1.0) + ramp.color_ramp.elements[1].position = 0.6 + ramp.color_ramp.elements[1].color = (*sky_color, 1.0) + + background = nodes.new('ShaderNodeBackground') + background.location = (100, 0) + background.inputs['Strength'].default_value = strength + + output = nodes.new('ShaderNodeOutputWorld') + output.location = (300, 0) + + links.new(tex_coord.outputs['Generated'], separate.inputs['Vector']) + links.new(separate.outputs['Z'], ramp.inputs['Fac']) + links.new(ramp.outputs['Color'], background.inputs['Color']) + links.new(background.outputs['Background'], output.inputs['Surface']) + + return world diff --git a/data/blender-scripts/import_neural_mesh.py b/data/blender-scripts/import_neural_mesh.py new file mode 100644 index 0000000..a01acc7 --- /dev/null +++ b/data/blender-scripts/import_neural_mesh.py @@ -0,0 +1,323 @@ +""" +Import Neural Mesh β€” Post-Processing Pipeline for AI-Generated 3D Models + +Handles the critical step of importing GLB/OBJ meshes produced by neural +generators (Hunyuan 3D, TRELLIS 2, YVO3D) into Blender and preparing them +for production use. + +Common issues with neural-generated meshes: +1. Non-manifold geometry (gaps, flipped normals, isolated vertices) +2. Excessive polygon count (100k-500k+ faces) +3. Missing or baked-in materials that need PBR conversion +4. No UV maps or auto-generated UVs with seam artifacts +5. Objects not centered at world origin +6. Scale inconsistencies (neural models have arbitrary units) + +This script addresses ALL of these issues. + +Categories: import, neural, mesh, cleanup, production +""" + +import bpy +import os +import math + + +# ============================================================================= +# 1. Import GLB/OBJ with Error Handling +# ============================================================================= + +def import_neural_mesh(filepath: str, name_prefix: str = "Neural") -> list: + """Import a GLB or OBJ file and return the imported mesh objects. + + Args: + filepath: Path to the .glb or .obj file + name_prefix: Prefix for renaming imported objects + + Returns: + List of imported mesh objects + """ + ext = os.path.splitext(filepath)[1].lower() + + # Track existing objects + existing = set(bpy.data.objects.keys()) + + if ext in ('.glb', '.gltf'): + bpy.ops.import_scene.gltf(filepath=filepath) + elif ext in ('.obj',): + bpy.ops.wm.obj_import(filepath=filepath) + elif ext in ('.fbx',): + bpy.ops.import_scene.fbx(filepath=filepath) + elif ext in ('.stl',): + bpy.ops.wm.stl_import(filepath=filepath) + else: + raise ValueError(f"Unsupported format: {ext}") + + # Identify newly imported objects + imported = [ + bpy.data.objects[name] + for name in bpy.data.objects.keys() + if name not in existing + ] + + # Filter to mesh objects and rename + meshes = [obj for obj in imported if obj.type == 'MESH'] + for i, obj in enumerate(meshes): + obj.name = f"{name_prefix}_{i:02d}" if len(meshes) > 1 else name_prefix + + print(f"Imported {len(meshes)} mesh objects from {os.path.basename(filepath)}") + return meshes + + +# ============================================================================= +# 2. Mesh Cleanup β€” Fix Common Neural Generation Artifacts +# ============================================================================= + +def cleanup_neural_mesh( + obj, + fix_normals: bool = True, + remove_doubles: bool = True, + merge_distance: float = 0.0001, + fill_holes: bool = True +): + """Clean up a neural-generated mesh. + + Neural models frequently produce: + - Flipped normals + - Duplicate vertices (near-zero distance apart) + - Non-manifold edges + - Loose vertices/edges + """ + if obj.type != 'MESH': + return + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + # Remove loose geometry + bpy.ops.mesh.delete_loose(use_verts=True, use_edges=True, use_faces=False) + + # Merge by distance (remove duplicate vertices) + if remove_doubles: + bpy.ops.mesh.remove_doubles(threshold=merge_distance) + + # Recalculate normals (neural meshes often have flipped faces) + if fix_normals: + bpy.ops.mesh.normals_make_consistent(inside=False) + + # Fill holes (non-manifold boundaries) + if fill_holes: + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_non_manifold() + try: + bpy.ops.mesh.fill() + except RuntimeError: + pass # Some non-manifold edges can't be filled + + bpy.ops.object.mode_set(mode='OBJECT') + print(f"Cleaned {obj.name}: {len(obj.data.vertices)} verts, {len(obj.data.polygons)} faces") + + +# ============================================================================= +# 3. Center and Scale to Standard Size +# ============================================================================= + +def normalize_neural_mesh( + obj, + target_height: float = 2.0, + ground_to_floor: bool = True +): + """Center the object at world origin and scale to a standard size. + + Neural models come in arbitrary units β€” this normalizes them. + """ + if obj.type != 'MESH': + return + + bpy.context.view_layer.objects.active = obj + + # Set origin to geometry center + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') + + # Center at world origin + obj.location = (0, 0, 0) + + # Scale to target height + dims = obj.dimensions + max_dim = max(dims) + if max_dim > 0: + scale_factor = target_height / max_dim + obj.scale = (scale_factor, scale_factor, scale_factor) + bpy.ops.object.transform_apply(scale=True) + + # Ground to floor (bottom of bounding box at z=0) + if ground_to_floor: + bbox_min_z = min(v.co.z for v in obj.data.vertices) + obj.location.z -= bbox_min_z + + print(f"Normalized {obj.name}: height={obj.dimensions.z:.2f}m, grounded={ground_to_floor}") + + +# ============================================================================= +# 4. Decimate for Target Poly Count +# ============================================================================= + +def decimate_to_target(obj, target_faces: int = 10000): + """Reduce polygon count while preserving shape. + + Neural meshes often have 100k-500k+ faces. This uses the Decimate + modifier to bring them to a usable count. + """ + if obj.type != 'MESH': + return + + current_faces = len(obj.data.polygons) + if current_faces <= target_faces: + print(f"{obj.name} already at {current_faces} faces (target: {target_faces})") + return + + ratio = target_faces / current_faces + + bpy.context.view_layer.objects.active = obj + mod = obj.modifiers.new(name="Neural_Decimate", type='DECIMATE') + mod.decimate_type = 'COLLAPSE' + mod.ratio = ratio + bpy.ops.object.modifier_apply(modifier=mod.name) + + final_faces = len(obj.data.polygons) + print(f"Decimated {obj.name}: {current_faces} β†’ {final_faces} faces (target: {target_faces})") + + +# ============================================================================= +# 5. Auto-UV for Neural Meshes +# ============================================================================= + +def auto_uv_neural_mesh(obj, method: str = 'SMART'): + """Generate UV maps for neural meshes that lack them. + + Args: + method: 'SMART' (Smart UV Project) or 'LIGHTMAP' (Lightmap Pack) + """ + if obj.type != 'MESH': + return + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + if method == 'SMART': + bpy.ops.uv.smart_project( + angle_limit=math.radians(66), + island_margin=0.02, + area_weight=0.0, + scale_to_bounds=True + ) + elif method == 'LIGHTMAP': + bpy.ops.uv.lightmap_pack( + PREF_CONTEXT='ALL_FACES', + PREF_PACK_IN_ONE=True, + PREF_BOX_DIV=12, + PREF_MARGIN_DIV=0.2 + ) + + bpy.ops.object.mode_set(mode='OBJECT') + print(f"UV unwrapped {obj.name} using {method}") + + +# ============================================================================= +# 6. Convert Baked Textures to PBR Material +# ============================================================================= + +def ensure_pbr_material(obj, material_name: str = "Neural_PBR"): + """Ensure the mesh has a proper PBR material setup. + + If the mesh came with vertex colors or baked textures, this preserves + them as the Base Color input while setting up proper PBR channels. + """ + if obj.type != 'MESH': + return + + # Check for existing materials + if len(obj.data.materials) > 0: + # Mesh already has materials β€” check if they're PBR-ready + mat = obj.data.materials[0] + if mat and mat.node_tree: + bsdf = mat.node_tree.nodes.get('Principled BSDF') + if bsdf: + print(f"{obj.name} already has a Principled BSDF material") + return + + # Create new PBR material + mat = bpy.data.materials.new(name=material_name) + bsdf = mat.node_tree.nodes.get('Principled BSDF') + + # If the mesh has vertex colors, connect them to Base Color + if obj.data.color_attributes: + attr_node = mat.node_tree.nodes.new('ShaderNodeVertexColor') + attr_node.layer_name = obj.data.color_attributes[0].name + mat.node_tree.links.new( + attr_node.outputs['Color'], + bsdf.inputs['Base Color'] + ) + + # Default PBR values + bsdf.inputs['Roughness'].default_value = 0.5 + bsdf.inputs['Metallic'].default_value = 0.0 + + if len(obj.data.materials) == 0: + obj.data.materials.append(mat) + else: + obj.data.materials[0] = mat + + print(f"Set up PBR material '{material_name}' on {obj.name}") + + +# ============================================================================= +# 7. Full Import Pipeline +# ============================================================================= + +def full_neural_import_pipeline( + filepath: str, + name: str = "Neural", + target_height: float = 2.0, + target_faces: int = 10000, + cleanup: bool = True, + auto_uv: bool = True, + setup_pbr: bool = True +) -> list: + """Complete pipeline: import β†’ cleanup β†’ normalize β†’ decimate β†’ UV β†’ PBR. + + Args: + filepath: Path to mesh file (GLB/OBJ/FBX/STL) + name: Name prefix for imported objects + target_height: Desired height in meters + target_faces: Target polygon count per object + cleanup: Whether to fix normals, remove doubles, etc. + auto_uv: Whether to generate UV maps + setup_pbr: Whether to ensure PBR material setup + + Returns: + List of processed mesh objects + """ + meshes = import_neural_mesh(filepath, name) + + for obj in meshes: + if cleanup: + cleanup_neural_mesh(obj) + normalize_neural_mesh(obj, target_height) + decimate_to_target(obj, target_faces) + if auto_uv: + auto_uv_neural_mesh(obj, method='SMART') + if setup_pbr: + ensure_pbr_material(obj, f"{name}_PBR") + + print(f"\n=== Neural Import Complete ===") + print(f"Objects: {len(meshes)}") + for obj in meshes: + print(f" {obj.name}: {len(obj.data.polygons)} faces, " + f"height={obj.dimensions.z:.2f}m") + + return meshes diff --git a/data/blender-scripts/interior_rooms.py b/data/blender-scripts/interior_rooms.py new file mode 100644 index 0000000..f9a107c --- /dev/null +++ b/data/blender-scripts/interior_rooms.py @@ -0,0 +1,259 @@ +""" +Interior Room & Architecture Recipes β€” Blender 4.0/5.0 +====================================================== +Category: utility +Blender: 4.0+ / 5.0 + +Recipes for creating enclosed interior rooms, walls, doorways, and +architectural structures. Essential for interior scene prompts +(taverns, houses, dungeons, offices, etc.). + +INTERIOR SCENE RULES: +1. ALWAYS create a proper enclosed room FIRST before placing furniture +2. Use separate mesh objects for each wall (easier to edit/remove for camera) +3. Floor + 4 walls + optional ceiling = minimum room shell +4. Scale to real-world units: wall height ~2.5-3m, door ~2m, table ~0.75m +5. Leave one wall open or use camera clipping for interior shots + +REAL-WORLD SCALE REFERENCE (for interiors): +| Element | Typical Size | +|-----------------|-----------------------| +| Room height | 2.5 – 3.0 m | +| Door | 2.0m H Γ— 0.9m W | +| Window | 1.2m H Γ— 1.0m W | +| Table | 0.75m H | +| Chair seat | 0.45m H | +| Person | 1.7 – 1.8m | +| Barrel | 0.9m H Γ— 0.5m Ø | +""" + +import bpy +import bmesh +import math + + +def create_room(name="Room", width=6, depth=8, height=3, + wall_thickness=0.2, open_front=False): + """Create an enclosed room with floor, 4 walls, and ceiling. + + Creates separate objects for floor, walls, and ceiling so individual + pieces can be hidden for camera placement. + + Args: + name: Base name for room objects + width: Room width (X axis) in meters + depth: Room depth (Y axis) in meters + height: Wall height in meters + wall_thickness: Thickness of walls + open_front: If True, omit front wall for camera access + Returns: + dict with 'floor', 'ceiling', 'walls' keys + """ + parts = {} + + # Floor + bpy.ops.mesh.primitive_plane_add(size=1, location=(0, 0, 0)) + floor = bpy.context.active_object + floor.name = f"{name}_Floor" + floor.scale = (width / 2, depth / 2, 1) + parts['floor'] = floor + + # Ceiling + bpy.ops.mesh.primitive_plane_add(size=1, location=(0, 0, height)) + ceiling = bpy.context.active_object + ceiling.name = f"{name}_Ceiling" + ceiling.scale = (width / 2, depth / 2, 1) + parts['ceiling'] = ceiling + + walls = [] + + # Back wall (Y+) + bpy.ops.mesh.primitive_cube_add(size=1, location=(0, depth / 2, height / 2)) + back = bpy.context.active_object + back.name = f"{name}_Wall_Back" + back.scale = (width / 2, wall_thickness / 2, height / 2) + walls.append(back) + + # Left wall (X-) + bpy.ops.mesh.primitive_cube_add( + size=1, location=(-width / 2, 0, height / 2)) + left = bpy.context.active_object + left.name = f"{name}_Wall_Left" + left.scale = (wall_thickness / 2, depth / 2, height / 2) + walls.append(left) + + # Right wall (X+) + bpy.ops.mesh.primitive_cube_add( + size=1, location=(width / 2, 0, height / 2)) + right = bpy.context.active_object + right.name = f"{name}_Wall_Right" + right.scale = (wall_thickness / 2, depth / 2, height / 2) + walls.append(right) + + # Front wall (Y-) β€” optional + if not open_front: + bpy.ops.mesh.primitive_cube_add( + size=1, location=(0, -depth / 2, height / 2)) + front = bpy.context.active_object + front.name = f"{name}_Wall_Front" + front.scale = (width / 2, wall_thickness / 2, height / 2) + walls.append(front) + + parts['walls'] = walls + return parts + + +def create_wall_with_doorway(name="DoorWall", width=6, height=3, + door_width=1.0, door_height=2.1, + door_offset=0.0, thickness=0.2): + """Create a wall with a rectangular doorway cut out. + + Uses BMesh to create a wall plane with a hole for the door. + The door is centered on door_offset from center of wall. + + Args: + door_width: Width of doorway opening + door_height: Height of doorway (standard: 2.1m) + door_offset: Horizontal offset from wall center (0 = centered) + thickness: Wall thickness + """ + mesh = bpy.data.meshes.new(f"{name}_Mesh") + obj = bpy.data.objects.new(name, mesh) + bpy.context.scene.collection.objects.link(obj) + + bm = bmesh.new() + + half_w = width / 2 + half_d = door_width / 2 + + # Create wall as 4 faces around the doorway opening + # Bottom-left section (below door isn't needed, door starts at floor) + # Left of door + if door_offset - half_d > -half_w: + verts_left = [ + bm.verts.new((-half_w, 0, 0)), + bm.verts.new((door_offset - half_d, 0, 0)), + bm.verts.new((door_offset - half_d, 0, height)), + bm.verts.new((-half_w, 0, height)), + ] + bm.faces.new(verts_left) + + # Right of door + if door_offset + half_d < half_w: + verts_right = [ + bm.verts.new((door_offset + half_d, 0, 0)), + bm.verts.new((half_w, 0, 0)), + bm.verts.new((half_w, 0, height)), + bm.verts.new((door_offset + half_d, 0, height)), + ] + bm.faces.new(verts_right) + + # Above door + verts_top = [ + bm.verts.new((door_offset - half_d, 0, door_height)), + bm.verts.new((door_offset + half_d, 0, door_height)), + bm.verts.new((door_offset + half_d, 0, height)), + bm.verts.new((door_offset - half_d, 0, height)), + ] + bm.faces.new(verts_top) + + bm.to_mesh(mesh) + bm.free() + mesh.update(calc_edges=True) + + # Add Solidify modifier for wall thickness + solidify = obj.modifiers.new(name="Solidify", type='SOLIDIFY') + solidify.thickness = thickness + solidify.offset = 0 # Center thickness on face + + return obj + + +def create_wall_with_window(name="WindowWall", width=6, height=3, + win_width=1.2, win_height=1.0, + win_bottom=1.0, win_offset=0.0, + thickness=0.2): + """Create a wall with a rectangular window opening. + + Args: + win_width: Window width + win_height: Window height + win_bottom: Height from floor to bottom of window (sill height) + win_offset: Horizontal offset from center + thickness: Wall thickness + """ + mesh = bpy.data.meshes.new(f"{name}_Mesh") + obj = bpy.data.objects.new(name, mesh) + bpy.context.scene.collection.objects.link(obj) + + bm = bmesh.new() + + half_w = width / 2 + half_win = win_width / 2 + win_top = win_bottom + win_height + wo = win_offset # shorthand + + # Below window (full width) + v_below = [ + bm.verts.new((-half_w, 0, 0)), + bm.verts.new((half_w, 0, 0)), + bm.verts.new((half_w, 0, win_bottom)), + bm.verts.new((-half_w, 0, win_bottom)), + ] + bm.faces.new(v_below) + + # Above window (full width) + v_above = [ + bm.verts.new((-half_w, 0, win_top)), + bm.verts.new((half_w, 0, win_top)), + bm.verts.new((half_w, 0, height)), + bm.verts.new((-half_w, 0, height)), + ] + bm.faces.new(v_above) + + # Left of window + if wo - half_win > -half_w: + v_left = [ + bm.verts.new((-half_w, 0, win_bottom)), + bm.verts.new((wo - half_win, 0, win_bottom)), + bm.verts.new((wo - half_win, 0, win_top)), + bm.verts.new((-half_w, 0, win_top)), + ] + bm.faces.new(v_left) + + # Right of window + if wo + half_win < half_w: + v_right = [ + bm.verts.new((wo + half_win, 0, win_bottom)), + bm.verts.new((half_w, 0, win_bottom)), + bm.verts.new((half_w, 0, win_top)), + bm.verts.new((wo + half_win, 0, win_top)), + ] + bm.faces.new(v_right) + + bm.to_mesh(mesh) + bm.free() + mesh.update(calc_edges=True) + + solidify = obj.modifiers.new(name="Solidify", type='SOLIDIFY') + solidify.thickness = thickness + + return obj + + +def create_stone_wall_material(name="StoneMaterial"): + """Quick stone/masonry material for walls.""" + mat = bpy.data.materials.new(name=name) + bsdf = mat.node_tree.nodes.get("Principled BSDF") + bsdf.inputs['Base Color'].default_value = (0.45, 0.42, 0.38, 1.0) + bsdf.inputs['Roughness'].default_value = 0.9 + return mat + + +def create_wood_material(name="WoodMaterial"): + """Quick warm wood material for floors and furniture.""" + mat = bpy.data.materials.new(name=name) + bsdf = mat.node_tree.nodes.get("Principled BSDF") + bsdf.inputs['Base Color'].default_value = (0.35, 0.22, 0.12, 1.0) + bsdf.inputs['Roughness'].default_value = 0.6 + return mat diff --git a/data/blender-scripts/model_export.py b/data/blender-scripts/model_export.py new file mode 100644 index 0000000..227dd7f --- /dev/null +++ b/data/blender-scripts/model_export.py @@ -0,0 +1,488 @@ +""" +{ + "title": "Production Model Export Pipeline", + "category": "export", + "tags": ["fbx", "gltf", "usd", "lod", "game-dev", "production", "pipeline", "format-presets"], + "description": "Production-grade model export with LOD chain generation, format presets (Game/VFX/Web/Print), USD export, pre-export validation, and scene bundle export. Extends basic export with game-dev and VFX workflows.", + "blender_version": "4.0+" +} +""" +import bpy +import os +import json + + +# --- Export Presets --- +EXPORT_PRESETS = { + 'game': { + 'description': 'Game engine export (Unity/Unreal)', + 'format': 'FBX', + 'triangulate': True, + 'apply_modifiers': True, + 'apply_transforms': True, + 'tangent_space': True, + 'embed_textures': False, + 'scale': 1.0 + }, + 'vfx': { + 'description': 'VFX pipeline export (Houdini/Maya)', + 'format': 'USD', + 'triangulate': False, + 'apply_modifiers': True, + 'apply_transforms': True, + 'tangent_space': False, + 'embed_textures': False, + 'scale': 1.0 + }, + 'web': { + 'description': 'Web 3D export (Three.js/Babylon)', + 'format': 'GLTF', + 'triangulate': True, + 'apply_modifiers': True, + 'apply_transforms': True, + 'tangent_space': True, + 'embed_textures': True, + 'scale': 1.0 + }, + 'print': { + 'description': '3D printing export', + 'format': 'OBJ', + 'triangulate': True, + 'apply_modifiers': True, + 'apply_transforms': True, + 'tangent_space': False, + 'embed_textures': False, + 'scale': 1000.0 # meters to mm + } +} + + +def generate_lods( + obj: bpy.types.Object, + lod_levels: int = 4, + ratios: list = None, + collection_name: str = None +) -> list: + """ + Generate a Level of Detail (LOD) chain from a high-poly mesh. + Creates LOD0 (source) through LOD3 with decreasing face counts. + + Args: + obj: Source mesh object (becomes LOD0) + lod_levels: Number of LOD levels to generate (2-5) + ratios: Custom decimation ratios per level. Default: [1.0, 0.5, 0.25, 0.125] + collection_name: Collection to put LODs in (created if needed) + + Returns: + List of LOD objects (LOD0 = original, LOD1+ = decimated copies) + + Example: + >>> lods = generate_lods(character, lod_levels=4) + >>> for lod in lods: + ... print(f"{lod.name}: {len(lod.data.polygons)} faces") + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + if ratios is None: + ratios = [1.0] + for i in range(1, lod_levels): + ratios.append(0.5 ** i) + + # Create collection for LODs + if collection_name is None: + collection_name = f"{obj.name}_LODs" + + if collection_name not in bpy.data.collections: + lod_collection = bpy.data.collections.new(collection_name) + bpy.context.scene.collection.children.link(lod_collection) + else: + lod_collection = bpy.data.collections[collection_name] + + lods = [] + + for i, ratio in enumerate(ratios): + if i == 0: + # LOD0 is the original + lod = obj + lod.name = f"{obj.name}_LOD0" + else: + # Duplicate and decimate + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.duplicate() + + lod = bpy.context.active_object + lod.name = f"{obj.name}_LOD{i}" + + # Apply Decimate modifier + mod = lod.modifiers.new(name=f"Decimate_LOD{i}", type='DECIMATE') + mod.ratio = ratio + bpy.ops.object.modifier_apply(modifier=mod.name) + + # Move to LOD collection + for col in lod.users_collection: + col.objects.unlink(lod) + lod_collection.objects.link(lod) + + # Offset for visibility + lod.location.x = obj.location.x + (i * max(obj.dimensions) * 1.5) + + lods.append(lod) + + # Link LOD0 to collection too + if obj.name not in [o.name for o in lod_collection.objects]: + lod_collection.objects.link(obj) + + return lods + + +def export_with_preset( + objects: list = None, + preset: str = 'game', + output_path: str = None, + filename: str = None +) -> str: + """ + Export using a named preset (Game, VFX, Web, Print). + + Args: + objects: List of objects to export (None = all selected) + preset: Preset name: 'game', 'vfx', 'web', 'print' + output_path: Output directory + filename: Output filename (without extension) + + Returns: + Path to exported file + + Example: + >>> export_with_preset(preset='game', output_path='/exports/') + >>> export_with_preset([character], preset='web', filename='hero_character') + """ + if preset not in EXPORT_PRESETS: + raise ValueError( + f"Unknown preset '{preset}'. Available: {', '.join(EXPORT_PRESETS.keys())}" + ) + + config = EXPORT_PRESETS[preset] + fmt = config['format'] + + if output_path is None: + output_path = os.path.dirname(bpy.data.filepath) or os.path.expanduser('~') + + if filename is None: + filename = os.path.splitext(os.path.basename(bpy.data.filepath))[0] or 'export' + + os.makedirs(output_path, exist_ok=True) + + # Select objects + if objects: + bpy.ops.object.select_all(action='DESELECT') + for obj in objects: + obj.select_set(True) + bpy.context.view_layer.objects.active = objects[0] + + # Pre-export: apply transforms + if config['apply_transforms']: + for obj in (objects or bpy.context.selected_objects): + bpy.context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + # Pre-export: triangulate + if config['triangulate']: + for obj in (objects or bpy.context.selected_objects): + if obj.type == 'MESH': + mod = obj.modifiers.new("Triangulate_Export", 'TRIANGULATE') + bpy.context.view_layer.objects.active = obj + bpy.ops.object.modifier_apply(modifier=mod.name) + + # Export based on format + if fmt == 'FBX': + filepath = os.path.join(output_path, f"{filename}.fbx") + bpy.ops.export_scene.fbx( + filepath=filepath, + use_selection=True, + apply_scale_options='FBX_SCALE_ALL', + use_mesh_modifiers=config['apply_modifiers'], + mesh_smooth_type='FACE', + use_tspace=config.get('tangent_space', True), + embed_textures=config.get('embed_textures', False) + ) + elif fmt == 'GLTF': + ext = '.glb' if config.get('embed_textures', True) else '.gltf' + filepath = os.path.join(output_path, f"{filename}{ext}") + bpy.ops.export_scene.gltf( + filepath=filepath, + export_format='GLB' if config.get('embed_textures', True) else 'GLTF_SEPARATE', + use_selection=True, + export_apply=config['apply_modifiers'] + ) + elif fmt == 'USD': + filepath = os.path.join(output_path, f"{filename}.usdc") + bpy.ops.wm.usd_export( + filepath=filepath, + selected_objects_only=True, + export_materials=True, + generate_preview_surface=True + ) + elif fmt == 'OBJ': + filepath = os.path.join(output_path, f"{filename}.obj") + bpy.ops.wm.obj_export( + filepath=filepath, + export_selected_objects=True, + apply_modifiers=config['apply_modifiers'], + export_triangulated_mesh=config['triangulate'], + global_scale=config.get('scale', 1.0) + ) + else: + raise ValueError(f"Unsupported format: {fmt}") + + return filepath + + +def export_usd( + filepath: str, + selected_only: bool = True, + export_materials: bool = True, + export_animation: bool = True, + export_hair: bool = False +) -> bool: + """ + Export to Universal Scene Description (USD/USDC/USDA). + USD is the standard for VFX pipelines (Pixar, Houdini, Omniverse). + + Args: + filepath: Output file path (.usdc, .usda, or .usd) + selected_only: Export only selected objects + export_materials: Include materials + export_animation: Include animation data + export_hair: Include particle hair + + Returns: + True if export succeeded + + Example: + >>> export_usd("/output/scene.usdc") + """ + try: + bpy.ops.wm.usd_export( + filepath=filepath, + selected_objects_only=selected_only, + export_materials=export_materials, + export_animation=export_animation, + export_hair=export_hair, + generate_preview_surface=True + ) + return True + except Exception: + return False + + +def validate_and_export( + obj: bpy.types.Object, + filepath: str, + format: str = 'FBX', + auto_fix: bool = True +) -> dict: + """ + Validate mesh for export issues, optionally fix them, then export. + + Checks: non-manifold geometry, missing UVs, unapplied scale, + ngons (for game export), loose vertices. + + Args: + obj: Object to validate and export + filepath: Export file path + format: 'FBX', 'GLTF', 'OBJ', 'USD' + auto_fix: Automatically fix issues before export + + Returns: + Dict with 'issues' list and 'exported' bool + + Example: + >>> result = validate_and_export(character, "/out/char.fbx") + >>> if result['issues']: + ... print("Warnings:", result['issues']) + """ + result = {'issues': [], 'exported': False, 'filepath': filepath} + + if obj.type != 'MESH': + result['issues'].append('Not a mesh object') + return result + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + mesh = obj.data + + # Check: Unapplied scale + if obj.scale[0] != 1.0 or obj.scale[1] != 1.0 or obj.scale[2] != 1.0: + result['issues'].append('Scale not applied') + if auto_fix: + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + + # Check: Missing UVs + if not mesh.uv_layers: + result['issues'].append('No UV maps') + if auto_fix: + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.smart_project(angle_limit=1.15, island_margin=0.02) + bpy.ops.object.mode_set(mode='OBJECT') + + # Check: Non-manifold + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_non_manifold() + if obj.data.total_vert_sel > 0: + result['issues'].append(f'Non-manifold geometry ({obj.data.total_vert_sel} verts)') + bpy.ops.object.mode_set(mode='OBJECT') + + # Check: Ngons + for poly in mesh.polygons: + if len(poly.vertices) > 4: + result['issues'].append('Contains N-gons') + break + + # Export + try: + format_upper = format.upper() + if format_upper == 'FBX': + bpy.ops.export_scene.fbx(filepath=filepath, use_selection=True) + elif format_upper in ('GLTF', 'GLB'): + bpy.ops.export_scene.gltf(filepath=filepath, use_selection=True) + elif format_upper == 'OBJ': + bpy.ops.wm.obj_export(filepath=filepath, export_selected_objects=True) + elif format_upper == 'USD': + bpy.ops.wm.usd_export(filepath=filepath, selected_objects_only=True) + result['exported'] = True + except Exception as e: + result['issues'].append(f'Export failed: {str(e)}') + + return result + + +def export_scene_bundle( + output_dir: str, + format: str = 'GLTF', + include_textures: bool = True, + separate_objects: bool = False +) -> dict: + """ + Export entire scene as an organized file bundle. + Creates a structured directory: meshes/, textures/, materials.json + + Args: + output_dir: Root output directory + format: Export format for meshes + include_textures: Copy textures to bundle + separate_objects: Export each object as individual file + + Returns: + Dict with 'mesh_files', 'texture_files', and 'manifest' path + + Example: + >>> bundle = export_scene_bundle("/output/my_scene/", format='GLTF') + """ + os.makedirs(output_dir, exist_ok=True) + mesh_dir = os.path.join(output_dir, 'meshes') + tex_dir = os.path.join(output_dir, 'textures') + os.makedirs(mesh_dir, exist_ok=True) + + result = {'mesh_files': [], 'texture_files': [], 'manifest': ''} + + # Export meshes + if separate_objects: + for obj in bpy.context.scene.objects: + if obj.type != 'MESH': + continue + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + clean_name = "".join(c for c in obj.name if c.isalnum() or c in ' -_').strip() + ext = '.glb' if format == 'GLTF' else f'.{format.lower()}' + filepath = os.path.join(mesh_dir, f"{clean_name}{ext}") + + if format == 'FBX': + bpy.ops.export_scene.fbx(filepath=filepath, use_selection=True) + elif format == 'GLTF': + bpy.ops.export_scene.gltf(filepath=filepath, use_selection=True, export_format='GLB') + elif format == 'OBJ': + bpy.ops.wm.obj_export(filepath=filepath, export_selected_objects=True) + + result['mesh_files'].append(filepath) + else: + bpy.ops.object.select_all(action='SELECT') + ext = '.glb' if format == 'GLTF' else f'.{format.lower()}' + filepath = os.path.join(mesh_dir, f"scene{ext}") + + if format == 'FBX': + bpy.ops.export_scene.fbx(filepath=filepath, use_selection=True) + elif format == 'GLTF': + bpy.ops.export_scene.gltf(filepath=filepath, use_selection=True, export_format='GLB') + elif format == 'OBJ': + bpy.ops.wm.obj_export(filepath=filepath, export_selected_objects=True) + + result['mesh_files'].append(filepath) + + # Copy textures + if include_textures: + os.makedirs(tex_dir, exist_ok=True) + for img in bpy.data.images: + if img.filepath and img.filepath != '' and not img.packed_file: + src = bpy.path.abspath(img.filepath) + if os.path.isfile(src): + import shutil + dst = os.path.join(tex_dir, os.path.basename(src)) + shutil.copy2(src, dst) + result['texture_files'].append(dst) + + # Write manifest + manifest = { + 'format': format, + 'objects': len(result['mesh_files']), + 'textures': len(result['texture_files']), + 'blender_version': bpy.app.version_string + } + manifest_path = os.path.join(output_dir, 'manifest.json') + with open(manifest_path, 'w') as f: + json.dump(manifest, f, indent=2) + result['manifest'] = manifest_path + + return result + + +def apply_export_transforms( + obj: bpy.types.Object, + triangulate: bool = True, + apply_scale: bool = True, + apply_rotation: bool = True +) -> None: + """ + Apply scale/rotation transforms and optionally triangulate for game engines. + + Args: + obj: Object to prepare + triangulate: Convert quads/ngons to triangles + apply_scale: Apply scale transform + apply_rotation: Apply rotation transform + + Example: + >>> apply_export_transforms(character_mesh, triangulate=True) + """ + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + bpy.ops.object.transform_apply( + location=False, + rotation=apply_rotation, + scale=apply_scale + ) + + if triangulate and obj.type == 'MESH': + mod = obj.modifiers.new("Triangulate_Export", 'TRIANGULATE') + mod.quad_method = 'BEAUTY' + mod.ngon_method = 'BEAUTY' + bpy.ops.object.modifier_apply(modifier=mod.name) diff --git a/data/blender-scripts/pbr_texture_loader.py b/data/blender-scripts/pbr_texture_loader.py new file mode 100644 index 0000000..77f7c87 --- /dev/null +++ b/data/blender-scripts/pbr_texture_loader.py @@ -0,0 +1,408 @@ +""" +{ + "title": "PBR Texture Loader and Baker", + "category": "materials", + "tags": ["pbr", "texture", "Principled BSDF", "material", "bake", "normal-map", "roughness", "metallic", "displacement"], + "description": "Load and apply PBR texture maps (albedo, roughness, metallic, normal, AO, displacement) to Blender's Principled BSDF shader. Also supports auto-discovery from folders, displacement setup, and texture baking.", + "blender_version": "4.0+" +} +""" +import bpy +import os + + +def apply_pbr_textures( + obj: bpy.types.Object, + albedo_path: str = None, + roughness_path: str = None, + metallic_path: str = None, + normal_path: str = None, + ao_path: str = None, + displacement_path: str = None, + material_name: str = "PBR_Material", + uv_scale: tuple = (1.0, 1.0, 1.0) +) -> bpy.types.Material: + """ + Create and apply a full PBR material from texture map files. + Connects all maps to the correct Principled BSDF inputs with + proper colorspace settings. + + Args: + obj: Mesh object to apply material to + albedo_path: Path to albedo/base color texture + roughness_path: Path to roughness map + metallic_path: Path to metallic map + normal_path: Path to normal map + ao_path: Path to ambient occlusion map + displacement_path: Path to displacement/height map + material_name: Name for the created material + uv_scale: UV tiling scale (x, y, z) + + Returns: + The created material + + Example: + >>> mat = apply_pbr_textures( + ... character_mesh, + ... albedo_path="/textures/skin_albedo.png", + ... normal_path="/textures/skin_normal.png", + ... roughness_path="/textures/skin_roughness.png" + ... ) + """ + mat = bpy.data.materials.new(name=material_name) + mat.use_nodes = True + nodes = mat.node_tree.nodes + links = mat.node_tree.links + + # Clear default nodes + nodes.clear() + + # Create Principled BSDF and Output + bsdf = nodes.new('ShaderNodeBsdfPrincipled') + bsdf.location = (0, 0) + output = nodes.new('ShaderNodeOutputMaterial') + output.location = (400, 0) + links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) + + # Create shared UV mapping node for tiling control + mapping = None + tex_coord = None + if uv_scale != (1.0, 1.0, 1.0): + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-1000, 0) + mapping = nodes.new('ShaderNodeMapping') + mapping.location = (-800, 0) + mapping.inputs['Scale'].default_value = uv_scale + links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) + + x_offset = -500 + y_pos = 300 + + # Helper to add a texture node + def add_texture(filepath, colorspace='sRGB', y=0): + nonlocal x_offset + if not filepath or not os.path.isfile(filepath): + return None + tex = nodes.new('ShaderNodeTexImage') + tex.location = (x_offset, y) + tex.image = bpy.data.images.load(filepath) + tex.image.colorspace_settings.name = colorspace + if mapping: + links.new(mapping.outputs['Vector'], tex.inputs['Vector']) + return tex + + # Albedo / Base Color + if albedo_path: + tex = add_texture(albedo_path, 'sRGB', y_pos) + if tex: + links.new(tex.outputs['Color'], bsdf.inputs['Base Color']) + y_pos -= 300 + + # Roughness + if roughness_path: + tex = add_texture(roughness_path, 'Non-Color', y_pos) + if tex: + links.new(tex.outputs['Color'], bsdf.inputs['Roughness']) + y_pos -= 300 + + # Metallic + if metallic_path: + tex = add_texture(metallic_path, 'Non-Color', y_pos) + if tex: + links.new(tex.outputs['Color'], bsdf.inputs['Metallic']) + y_pos -= 300 + + # Normal Map + if normal_path: + tex = add_texture(normal_path, 'Non-Color', y_pos) + if tex: + normal_map = nodes.new('ShaderNodeNormalMap') + normal_map.location = (-200, y_pos) + links.new(tex.outputs['Color'], normal_map.inputs['Color']) + links.new(normal_map.outputs['Normal'], bsdf.inputs['Normal']) + y_pos -= 300 + + # Ambient Occlusion (multiply with Base Color if both exist) + if ao_path: + tex = add_texture(ao_path, 'Non-Color', y_pos) + if tex and albedo_path: + mix = nodes.new('ShaderNodeMix') + mix.data_type = 'RGBA' + mix.location = (-200, y_pos) + mix.blend_type = 'MULTIPLY' + mix.inputs[0].default_value = 1.0 # Factor + + # Reconnect: albedo β†’ mix A, AO β†’ mix B, mix β†’ Base Color + for link in list(links): + if link.to_socket == bsdf.inputs['Base Color']: + links.new(link.from_socket, mix.inputs[6]) # A + links.remove(link) + break + links.new(tex.outputs['Color'], mix.inputs[7]) # B + links.new(mix.outputs[2], bsdf.inputs['Base Color']) # Result + y_pos -= 300 + + # Displacement + if displacement_path: + tex = add_texture(displacement_path, 'Non-Color', y_pos) + if tex: + disp = nodes.new('ShaderNodeDisplacement') + disp.location = (0, y_pos) + disp.inputs['Scale'].default_value = 0.1 + disp.inputs['Midlevel'].default_value = 0.5 + links.new(tex.outputs['Color'], disp.inputs['Height']) + links.new(disp.outputs['Displacement'], output.inputs['Displacement']) + mat.cycles.displacement_method = 'BOTH' + + # Assign to object + if obj.data.materials: + obj.data.materials[0] = mat + else: + obj.data.materials.append(mat) + + return mat + + +def create_pbr_material_from_folder( + obj: bpy.types.Object, + folder_path: str, + material_name: str = None +) -> bpy.types.Material: + """ + Auto-discover PBR texture maps in a folder by naming convention + and apply them. Recognizes common suffixes: + _albedo, _basecolor, _diffuse, _color β†’ Base Color + _roughness, _rough β†’ Roughness + _metallic, _metal, _metalness β†’ Metallic + _normal, _norm, _nrm β†’ Normal + _ao, _occlusion, _ambient β†’ AO + _displacement, _disp, _height β†’ Displacement + + Args: + obj: Mesh object to apply material to + folder_path: Directory containing texture files + material_name: Material name (default: folder name) + + Returns: + The created material + + Example: + >>> mat = create_pbr_material_from_folder(mesh, "/textures/wood_planks/") + """ + if not os.path.isdir(folder_path): + raise ValueError(f"Directory not found: {folder_path}") + + if material_name is None: + material_name = os.path.basename(folder_path.rstrip('/\\')) + + # Map suffixes to their roles + suffix_map = { + 'albedo': 'albedo', 'basecolor': 'albedo', 'base_color': 'albedo', + 'diffuse': 'albedo', 'color': 'albedo', 'col': 'albedo', + 'roughness': 'roughness', 'rough': 'roughness', + 'metallic': 'metallic', 'metal': 'metallic', 'metalness': 'metallic', + 'normal': 'normal', 'norm': 'normal', 'nrm': 'normal', 'nor': 'normal', + 'ao': 'ao', 'occlusion': 'ao', 'ambient_occlusion': 'ao', + 'displacement': 'displacement', 'disp': 'displacement', 'height': 'displacement', + } + + image_extensions = {'.png', '.jpg', '.jpeg', '.tif', '.tiff', '.exr', '.hdr', '.bmp'} + + discovered = {} + + for filename in os.listdir(folder_path): + ext = os.path.splitext(filename)[1].lower() + if ext not in image_extensions: + continue + + name_lower = os.path.splitext(filename)[0].lower() + for suffix, role in suffix_map.items(): + if suffix in name_lower.split('_'): + discovered[role] = os.path.join(folder_path, filename) + break + + return apply_pbr_textures( + obj, + albedo_path=discovered.get('albedo'), + roughness_path=discovered.get('roughness'), + metallic_path=discovered.get('metallic'), + normal_path=discovered.get('normal'), + ao_path=discovered.get('ao'), + displacement_path=discovered.get('displacement'), + material_name=material_name + ) + + +def apply_displacement_from_texture( + obj: bpy.types.Object, + height_map_path: str, + strength: float = 0.5, + subdivision_levels: int = 3 +) -> None: + """ + Apply displacement modifier driven by a height/displacement texture. + Adds Subdivision Surface + Displace modifiers for real geometry displacement. + + Args: + obj: Mesh object + height_map_path: Path to height map image (grayscale) + strength: Displacement intensity + subdivision_levels: Subdivision level for detail (2-4 recommended) + + Example: + >>> apply_displacement_from_texture(terrain, "/maps/terrain_height.png", strength=2.0) + """ + if obj.type != 'MESH': + raise ValueError(f"Object '{obj.name}' is not a mesh") + + # Add subdivision for geometry detail + subsurf = obj.modifiers.new(name="Subsurf_Disp", type='SUBSURF') + subsurf.levels = subdivision_levels + subsurf.render_levels = subdivision_levels + + # Load height map as texture + img = bpy.data.images.load(height_map_path) + img.colorspace_settings.name = 'Non-Color' + + tex = bpy.data.textures.new(name="Displacement_Tex", type='IMAGE') + tex.image = img + + # Add Displace modifier + displace = obj.modifiers.new(name="Displace_Map", type='DISPLACE') + displace.texture = tex + displace.strength = strength + displace.mid_level = 0.5 + displace.texture_coords = 'UV' + + +def setup_texture_mapping( + material: bpy.types.Material, + scale: tuple = (1.0, 1.0, 1.0), + rotation: tuple = (0.0, 0.0, 0.0), + offset: tuple = (0.0, 0.0, 0.0) +) -> None: + """ + Configure UV mapping for all texture nodes in a material. + Adds or updates a shared Mapping node connected to all image textures. + + Args: + material: Material to update + scale: UV tiling scale + rotation: UV rotation in radians + offset: UV offset + + Example: + >>> setup_texture_mapping(wood_material, scale=(2, 2, 1), rotation=(0, 0, 0.785)) + """ + if not material.use_nodes: + return + + nodes = material.node_tree.nodes + links = material.node_tree.links + + # Find or create TexCoord and Mapping nodes + tex_coord = None + mapping = None + for node in nodes: + if node.type == 'TEX_COORD': + tex_coord = node + elif node.type == 'MAPPING': + mapping = node + + if not tex_coord: + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-1000, 0) + + if not mapping: + mapping = nodes.new('ShaderNodeMapping') + mapping.location = (-800, 0) + + mapping.inputs['Scale'].default_value = scale + mapping.inputs['Rotation'].default_value = rotation + mapping.inputs['Location'].default_value = offset + + links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) + + # Connect mapping to all image texture nodes + for node in nodes: + if node.type == 'TEX_IMAGE': + links.new(mapping.outputs['Vector'], node.inputs['Vector']) + + +def bake_textures( + obj: bpy.types.Object, + output_dir: str, + bake_types: list = None, + resolution: int = 1024, + margin: int = 16 +) -> dict: + """ + Bake Cycles render data to texture maps. + + Args: + obj: Object to bake from + output_dir: Directory to save baked textures + bake_types: List of bake types. Options: 'DIFFUSE', 'ROUGHNESS', + 'NORMAL', 'AO', 'EMIT', 'COMBINED', 'SHADOW' + resolution: Texture resolution in pixels + margin: Bleed margin in pixels + + Returns: + Dict mapping bake type to output file path + + Example: + >>> paths = bake_textures(character, "/output/bakes/", ['DIFFUSE', 'NORMAL', 'AO']) + """ + if bake_types is None: + bake_types = ['DIFFUSE', 'NORMAL', 'AO'] + + os.makedirs(output_dir, exist_ok=True) + + # Ensure Cycles is active + bpy.context.scene.render.engine = 'CYCLES' + bpy.context.scene.cycles.device = 'GPU' + bpy.context.scene.cycles.samples = 128 + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + results = {} + + for bake_type in bake_types: + # Create bake target image + img_name = f"{obj.name}_{bake_type.lower()}" + img = bpy.data.images.new(img_name, resolution, resolution) + + # Create temporary image texture node in material + mat = obj.active_material + if not mat: + continue + + nodes = mat.node_tree.nodes + bake_node = nodes.new('ShaderNodeTexImage') + bake_node.image = img + bake_node.select = True + nodes.active = bake_node + + # Bake + try: + bpy.ops.object.bake( + type=bake_type, + margin=margin, + use_clear=True + ) + + # Save image + filepath = os.path.join(output_dir, f"{img_name}.png") + img.filepath_raw = filepath + img.file_format = 'PNG' + img.save() + results[bake_type] = filepath + + except RuntimeError as e: + results[bake_type] = f"ERROR: {str(e)}" + + # Cleanup temp node + nodes.remove(bake_node) + + return results diff --git a/data/blender-scripts/photorealistic_materials.py b/data/blender-scripts/photorealistic_materials.py new file mode 100644 index 0000000..556550d --- /dev/null +++ b/data/blender-scripts/photorealistic_materials.py @@ -0,0 +1,311 @@ +""" +Photorealistic PBR Texture Materials β€” Blender 4.0/5.0 +====================================================== +Category: materials +Blender: 4.0+ / 5.0 +Source: cgbookcase.com PBR guide + Blender docs + +Complete image-based PBR material workflows for photorealistic rendering. +Teaches how to set up materials using texture maps (not just procedural). + +PBR TEXTURE MAP CONNECTIONS: +| Map Type | Destination | Color Space | +|----------------|--------------------------------|-------------| +| Base Color | Principled BSDF β†’ Base Color | sRGB/Color | +| Roughness | Principled BSDF β†’ Roughness | Non-Color | +| Metallic | Principled BSDF β†’ Metallic | Non-Color | +| Normal Map | Normal Map node β†’ Principled | Non-Color | +| Height/Bump | Bump node β†’ Principled Normal | Non-Color | +| Displacement | Displacement β†’ Material Output | Non-Color | +| AO | Mix(Multiply) with Base Color | Non-Color | +| Opacity | Mix Shader (Principled+Transp) | Non-Color | + +CRITICAL RULES: +- ALL non-color maps MUST have color_space set to 'Non-Color' +- Base Color is the ONLY map that stays 'sRGB' (or 'Color') +- Normal maps need a Normal Map node (NOT directly to Principled) +- Height maps need either Bump node (fake) or Displacement node (real) +""" + +import bpy +import os + + +def create_pbr_material(name, textures_dir, + base_color_file=None, + roughness_file=None, + metallic_file=None, + normal_file=None, + height_file=None, + ao_file=None, + displacement_scale=0.1): + """Create a full PBR material from image texture files. + + This function creates a production-ready PBR material by loading + texture map files and connecting them to the Principled BSDF. + + Args: + name: Material name + textures_dir: Directory containing texture files + base_color_file: Filename for albedo/diffuse map (sRGB) + roughness_file: Filename for roughness map (Non-Color) + metallic_file: Filename for metallic map (Non-Color) + normal_file: Filename for normal map (Non-Color) + height_file: Filename for height/bump map (Non-Color) + ao_file: Filename for ambient occlusion map (Non-Color) + displacement_scale: Scale for true displacement + """ + mat = bpy.data.materials.new(name=name) + nodes = mat.node_tree.nodes + links = mat.node_tree.links + nodes.clear() + + # Core nodes + output = nodes.new('ShaderNodeOutputMaterial') + output.location = (800, 0) + bsdf = nodes.new('ShaderNodeBsdfPrincipled') + bsdf.location = (400, 0) + links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) + + # Texture Coordinate + Mapping for UV control + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-1200, 0) + mapping = nodes.new('ShaderNodeMapping') + mapping.location = (-1000, 0) + links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) + + y_offset = 300 + x_base = -600 + + # --- Base Color (sRGB) --- + if base_color_file: + bc_tex = _add_image_texture( + nodes, links, mapping, + os.path.join(textures_dir, base_color_file), + color_space='sRGB', + location=(x_base, y_offset) + ) + # If AO map exists, multiply it with base color + if ao_file: + ao_tex = _add_image_texture( + nodes, links, mapping, + os.path.join(textures_dir, ao_file), + color_space='Non-Color', + location=(x_base, y_offset - 200) + ) + mix = nodes.new('ShaderNodeMix') + mix.data_type = 'RGBA' + mix.blend_type = 'MULTIPLY' + mix.location = (x_base + 300, y_offset) + mix.inputs[0].default_value = 1.0 # Factor + links.new(bc_tex.outputs['Color'], mix.inputs[6]) # A + links.new(ao_tex.outputs['Color'], mix.inputs[7]) # B + links.new(mix.outputs[2], bsdf.inputs['Base Color']) + else: + links.new(bc_tex.outputs['Color'], bsdf.inputs['Base Color']) + + # --- Roughness (Non-Color) --- + if roughness_file: + rough_tex = _add_image_texture( + nodes, links, mapping, + os.path.join(textures_dir, roughness_file), + color_space='Non-Color', + location=(x_base, y_offset - 400) + ) + links.new(rough_tex.outputs['Color'], bsdf.inputs['Roughness']) + + # --- Metallic (Non-Color) --- + if metallic_file: + metal_tex = _add_image_texture( + nodes, links, mapping, + os.path.join(textures_dir, metallic_file), + color_space='Non-Color', + location=(x_base, y_offset - 600) + ) + links.new(metal_tex.outputs['Color'], bsdf.inputs['Metallic']) + + # --- Normal Map (Non-Color, needs Normal Map node) --- + if normal_file: + norm_tex = _add_image_texture( + nodes, links, mapping, + os.path.join(textures_dir, normal_file), + color_space='Non-Color', + location=(x_base, y_offset - 800) + ) + normal_node = nodes.new('ShaderNodeNormalMap') + normal_node.location = (x_base + 300, y_offset - 800) + normal_node.inputs['Strength'].default_value = 1.0 + links.new(norm_tex.outputs['Color'], normal_node.inputs['Color']) + links.new(normal_node.outputs['Normal'], bsdf.inputs['Normal']) + + # --- Height/Bump (Non-Color, uses Bump node) --- + if height_file and not normal_file: + # Use Bump node only if no Normal Map (they're redundant together) + height_tex = _add_image_texture( + nodes, links, mapping, + os.path.join(textures_dir, height_file), + color_space='Non-Color', + location=(x_base, y_offset - 1000) + ) + bump = nodes.new('ShaderNodeBump') + bump.location = (x_base + 300, y_offset - 1000) + bump.inputs['Strength'].default_value = 0.5 + links.new(height_tex.outputs['Color'], bump.inputs['Height']) + links.new(bump.outputs['Normal'], bsdf.inputs['Normal']) + + # --- True Displacement (Cycles only) --- + if height_file: + mat.displacement_method = 'BOTH' + height_tex_disp = _add_image_texture( + nodes, links, mapping, + os.path.join(textures_dir, height_file), + color_space='Non-Color', + location=(x_base, y_offset - 1200) + ) + disp = nodes.new('ShaderNodeDisplacement') + disp.location = (400, -300) + disp.inputs['Scale'].default_value = displacement_scale + disp.inputs['Midlevel'].default_value = 0.5 + links.new(height_tex_disp.outputs['Color'], disp.inputs['Height']) + links.new(disp.outputs['Displacement'], output.inputs['Displacement']) + + return mat + + +def _add_image_texture(nodes, links, mapping_node, filepath, + color_space='sRGB', location=(0, 0)): + """Helper: create an Image Texture node, load image, connect mapping.""" + tex = nodes.new('ShaderNodeTexImage') + tex.location = location + tex.image = bpy.data.images.load(filepath) + tex.image.colorspace_settings.name = color_space + links.new(mapping_node.outputs['Vector'], tex.inputs['Vector']) + return tex + + +# ============================================================================= +# PROCEDURAL PHOTOREALISTIC MATERIALS (no image files needed) +# ============================================================================= + +def create_procedural_stone_wall(name="StoneWall"): + """Create photorealistic stone wall using only procedural textures. + + Uses layered noise + voronoi for stone pattern with mortar gaps. + No image files needed β€” everything is generated procedurally. + """ + mat = bpy.data.materials.new(name=name) + mat.displacement_method = 'BOTH' + nodes = mat.node_tree.nodes + links = mat.node_tree.links + nodes.clear() + + output = nodes.new('ShaderNodeOutputMaterial') + output.location = (800, 0) + bsdf = nodes.new('ShaderNodeBsdfPrincipled') + bsdf.location = (400, 0) + bsdf.inputs['Roughness'].default_value = 0.85 + links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) + + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-800, 0) + + # Voronoi for stone cell pattern + voronoi = nodes.new('ShaderNodeTexVoronoi') + voronoi.location = (-400, 200) + voronoi.feature = 'F1' + voronoi.inputs['Scale'].default_value = 4.0 + voronoi.inputs['Randomness'].default_value = 0.8 + + # Noise for color variation + noise = nodes.new('ShaderNodeTexNoise') + noise.location = (-400, 0) + noise.inputs['Scale'].default_value = 15.0 + noise.inputs['Detail'].default_value = 6.0 + + # Color ramp for stone vs mortar + ramp = nodes.new('ShaderNodeValToRGB') + ramp.location = (-100, 200) + ramp.color_ramp.elements[0].position = 0.05 + ramp.color_ramp.elements[0].color = (0.15, 0.12, 0.10, 1) # Mortar + ramp.color_ramp.elements[1].position = 0.1 + ramp.color_ramp.elements[1].color = (0.5, 0.45, 0.38, 1) # Stone + + # Mix stone color with noise variation + mix_color = nodes.new('ShaderNodeMix') + mix_color.location = (100, 100) + mix_color.data_type = 'RGBA' + mix_color.inputs[0].default_value = 0.3 + + links.new(tex_coord.outputs['Object'], voronoi.inputs['Vector']) + links.new(tex_coord.outputs['Object'], noise.inputs['Vector']) + links.new(voronoi.outputs['Distance'], ramp.inputs['Fac']) + links.new(ramp.outputs['Color'], mix_color.inputs[6]) + links.new(noise.outputs['Color'], mix_color.inputs[7]) + links.new(mix_color.outputs[2], bsdf.inputs['Base Color']) + + # Displacement from voronoi distance + disp = nodes.new('ShaderNodeDisplacement') + disp.location = (400, -200) + disp.inputs['Scale'].default_value = 0.05 + links.new(voronoi.outputs['Distance'], disp.inputs['Height']) + links.new(disp.outputs['Displacement'], output.inputs['Displacement']) + + return mat + + +def create_procedural_wood_floor(name="WoodFloor"): + """Create procedural wood planks material. + + Uses Wave Texture in BANDS mode for wood grain pattern. + """ + mat = bpy.data.materials.new(name=name) + nodes = mat.node_tree.nodes + links = mat.node_tree.links + nodes.clear() + + output = nodes.new('ShaderNodeOutputMaterial') + output.location = (800, 0) + bsdf = nodes.new('ShaderNodeBsdfPrincipled') + bsdf.location = (400, 0) + bsdf.inputs['Roughness'].default_value = 0.55 + links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) + + tex_coord = nodes.new('ShaderNodeTexCoord') + tex_coord.location = (-800, 0) + + # Wave for grain lines + wave = nodes.new('ShaderNodeTexWave') + wave.location = (-400, 100) + wave.wave_type = 'BANDS' + wave.wave_profile = 'SAW' + wave.bands_direction = 'Y' + wave.inputs['Scale'].default_value = 2.0 + wave.inputs['Distortion'].default_value = 8.0 + wave.inputs['Detail'].default_value = 3.0 + wave.inputs['Detail Scale'].default_value = 1.5 + + # Color ramp for wood tones + ramp = nodes.new('ShaderNodeValToRGB') + ramp.location = (-100, 100) + ramp.color_ramp.elements[0].color = (0.25, 0.15, 0.08, 1) # Dark wood + ramp.color_ramp.elements[1].color = (0.55, 0.35, 0.18, 1) # Light wood + + # Subtle noise for grain variation + noise = nodes.new('ShaderNodeTexNoise') + noise.location = (-400, -100) + noise.inputs['Scale'].default_value = 50.0 + noise.inputs['Detail'].default_value = 4.0 + + # Bump for surface texture + bump = nodes.new('ShaderNodeBump') + bump.location = (100, -200) + bump.inputs['Strength'].default_value = 0.3 + + links.new(tex_coord.outputs['Object'], wave.inputs['Vector']) + links.new(tex_coord.outputs['Object'], noise.inputs['Vector']) + links.new(wave.outputs['Fac'], ramp.inputs['Fac']) + links.new(ramp.outputs['Color'], bsdf.inputs['Base Color']) + links.new(noise.outputs['Fac'], bump.inputs['Height']) + links.new(bump.outputs['Normal'], bsdf.inputs['Normal']) + + return mat diff --git a/data/blender-scripts/procedural_animation.py b/data/blender-scripts/procedural_animation.py new file mode 100644 index 0000000..e16e29f --- /dev/null +++ b/data/blender-scripts/procedural_animation.py @@ -0,0 +1,415 @@ +""" +{ + "title": "Procedural Animation Patterns", + "category": "animation", + "tags": ["procedural", "keyframe", "NLA", "orbit", "wave", "pendulum", "spring", "math", "camera", "dolly-zoom"], + "description": "Math-driven keyframe animation patterns for common motion types: orbit, wave, pendulum, spring, camera dolly zoom. Also includes NLA track composition for layering animations and production animation setup.", + "blender_version": "4.0+" +} +""" +import bpy +import math + + +def orbit_animation( + obj: bpy.types.Object, + center: tuple = (0, 0, 0), + radius: float = 5.0, + duration: int = 120, + start_frame: int = 1, + axis: str = 'Z', + revolutions: int = 1, + tilt: float = 0.0 +) -> None: + """ + Animate an object orbiting around a center point. + Perfect for planets, cameras, or particle-like motion. + + Args: + obj: Object to animate + center: Center point of the orbit (x, y, z) + radius: Orbit radius + duration: Total frames for one revolution + start_frame: Starting frame + axis: Orbit axis ('X', 'Y', 'Z') + revolutions: Number of complete orbits + tilt: Tilt angle in degrees (for inclined orbits) + + Example: + >>> orbit_animation(camera, center=(0,0,0), radius=10, duration=240) + >>> orbit_animation(moon, center=planet.location, radius=3, revolutions=2) + """ + total_frames = duration * revolutions + tilt_rad = math.radians(tilt) + + for frame in range(start_frame, start_frame + total_frames + 1): + t = (frame - start_frame) / duration + angle = t * 2 * math.pi + + if axis == 'Z': + x = center[0] + radius * math.cos(angle) + y = center[1] + radius * math.sin(angle) + z = center[2] + radius * math.sin(tilt_rad) * math.sin(angle) + elif axis == 'Y': + x = center[0] + radius * math.cos(angle) + y = center[1] + z = center[2] + radius * math.sin(angle) + else: # X axis + x = center[0] + y = center[1] + radius * math.cos(angle) + z = center[2] + radius * math.sin(angle) + + obj.location = (x, y, z) + obj.keyframe_insert(data_path="location", frame=frame) + + +def wave_animation( + obj: bpy.types.Object, + axis: str = 'Z', + amplitude: float = 1.0, + frequency: float = 1.0, + duration: int = 60, + start_frame: int = 1, + phase: float = 0.0, + damping: float = 0.0 +) -> None: + """ + Sine wave motion on a single axis. + Great for flags, water surfaces, breathing, floating objects. + + Args: + obj: Object to animate + axis: Motion axis ('X', 'Y', 'Z') + amplitude: Wave height + frequency: Oscillation speed (1.0 = one full cycle per duration) + duration: Total frames + start_frame: Starting frame + phase: Phase offset in radians + damping: Exponential decay (0 = no damping, higher = faster decay) + + Example: + >>> wave_animation(buoy, axis='Z', amplitude=0.5, frequency=2) + >>> wave_animation(flag_tip, axis='X', amplitude=0.3, damping=0.02) + """ + axis_index = {'X': 0, 'Y': 1, 'Z': 2}[axis.upper()] + base_value = obj.location[axis_index] + + for frame in range(start_frame, start_frame + duration + 1): + t = (frame - start_frame) / duration + angle = t * frequency * 2 * math.pi + phase + decay = math.exp(-damping * (frame - start_frame)) + value = base_value + amplitude * math.sin(angle) * decay + + obj.location[axis_index] = value + obj.keyframe_insert(data_path="location", index=axis_index, frame=frame) + + +def pendulum_animation( + obj: bpy.types.Object, + swing_axis: str = 'Y', + max_angle: float = 45.0, + duration: int = 60, + start_frame: int = 1, + damping: float = 0.02, + cycles: int = 5 +) -> None: + """ + Pendulum swing animation with optional damping. + Natural-looking for hanging objects, chandeliers, wrecking balls. + + Args: + obj: Object to animate (pivot should be at object origin) + swing_axis: Rotation axis ('X', 'Y', 'Z') + max_angle: Maximum swing angle in degrees + duration: Total frames + start_frame: Starting frame + damping: How fast the swing dies out (0 = perpetual) + cycles: Number of full swing cycles + + Example: + >>> pendulum_animation(chandelier, max_angle=30, damping=0.03) + """ + axis_index = {'X': 0, 'Y': 1, 'Z': 2}[swing_axis.upper()] + frequency = cycles * 2 * math.pi / duration + + for frame in range(start_frame, start_frame + duration + 1): + t = frame - start_frame + decay = math.exp(-damping * t) + angle = math.radians(max_angle) * math.cos(frequency * t) * decay + + obj.rotation_euler[axis_index] = angle + obj.keyframe_insert(data_path="rotation_euler", index=axis_index, frame=frame) + + +def spring_animation( + obj: bpy.types.Object, + axis: str = 'Z', + displacement: float = 2.0, + stiffness: float = 8.0, + damping: float = 0.1, + duration: int = 60, + start_frame: int = 1 +) -> None: + """ + Damped spring oscillation β€” jelly bounce, impact reaction, squash & stretch. + + Args: + obj: Object to animate + axis: Motion axis ('X', 'Y', 'Z') + displacement: Initial displacement from rest position + stiffness: Spring stiffness (higher = faster oscillation) + damping: Damping coefficient (higher = faster settling) + duration: Total frames + start_frame: Starting frame + + Example: + >>> spring_animation(jelly_cube, displacement=1.5, stiffness=10, damping=0.15) + """ + axis_index = {'X': 0, 'Y': 1, 'Z': 2}[axis.upper()] + base = obj.location[axis_index] + omega = math.sqrt(max(0.01, stiffness - damping ** 2)) + + for frame in range(start_frame, start_frame + duration + 1): + t = (frame - start_frame) / 24.0 # Normalize to seconds at 24fps + decay = math.exp(-damping * t) + value = base + displacement * decay * math.cos(omega * t) + + obj.location[axis_index] = value + obj.keyframe_insert(data_path="location", index=axis_index, frame=frame) + + +def follow_path_with_banking( + obj: bpy.types.Object, + curve: bpy.types.Object, + duration: int = 120, + bank_angle: float = 15.0, + start_frame: int = 1 +) -> bpy.types.Constraint: + """ + Animate object along a curve with banking (tilt on turns). + Ideal for vehicles, aircraft, roller coasters. + + Args: + obj: Object to animate along the path + curve: Bezier/NURBS curve to follow + duration: Total animation frames + bank_angle: Maximum bank angle on turns (degrees) + start_frame: Starting frame + + Returns: + The Follow Path constraint + + Example: + >>> follow_path_with_banking(airplane, flight_path, duration=300, bank_angle=25) + """ + # Set curve animation duration + curve.data.path_duration = duration + + # Add Follow Path constraint + constraint = obj.constraints.new('FOLLOW_PATH') + constraint.target = curve + constraint.use_curve_follow = True + constraint.forward_axis = 'FORWARD_Y' + constraint.up_axis = 'UP_Z' + + # Animate offset + constraint.offset = 0 + constraint.keyframe_insert('offset', frame=start_frame) + constraint.offset = -100 + constraint.keyframe_insert('offset', frame=start_frame + duration) + + # Set linear interpolation for smooth motion + if obj.animation_data and obj.animation_data.action: + for fcurve in obj.animation_data.action.fcurves: + for kf in fcurve.keyframe_points: + kf.interpolation = 'LINEAR' + + # Add banking via tilt on the curve + if bank_angle > 0: + curve.data.twist_mode = 'MINIMUM' + # Banking is handled by the Follow Path constraint's tilt + + return constraint + + +def camera_dolly_zoom( + camera: bpy.types.Object, + target: bpy.types.Object, + start_focal: float = 24.0, + end_focal: float = 70.0, + duration: int = 60, + start_frame: int = 1 +) -> None: + """ + Dolly zoom (Hitchcock / Vertigo effect). + Camera moves toward/away from subject while changing focal length + to keep the subject the same size but distort perspective. + + Args: + camera: Camera object + target: Object to keep in frame + start_focal: Starting focal length in mm + end_focal: Ending focal length in mm + duration: Total frames + start_frame: Starting frame + + Example: + >>> camera_dolly_zoom(cam, character, start_focal=24, end_focal=85) + """ + if camera.type != 'CAMERA': + raise ValueError("Object must be a camera") + + cam_data = camera.data + target_pos = target.location + + # Calculate initial distance (to keep subject same apparent size) + initial_distance = (camera.location - target_pos).length + + for frame in range(start_frame, start_frame + duration + 1): + t = (frame - start_frame) / duration + + # Interpolate focal length + focal = start_focal + (end_focal - start_focal) * t + cam_data.lens = focal + cam_data.keyframe_insert(data_path="lens", frame=frame) + + # Adjust distance to keep subject same apparent size + # Distance proportional to focal length + new_distance = initial_distance * (focal / start_focal) + + # Move camera along its forward axis + direction = (camera.location - target_pos).normalized() + camera.location = target_pos + direction * new_distance + camera.keyframe_insert(data_path="location", frame=frame) + + +def nla_compose( + obj: bpy.types.Object, + actions: list, + blend_modes: list = None +) -> list: + """ + Push multiple actions to NLA tracks for layered animation. + NLA lets you combine, blend, and sequence multiple animations + (e.g., walk cycle + arm wave + procedural noise). + + Args: + obj: Animated object + actions: List of dicts with 'action' (bpy.types.Action), 'name' (str), + 'start' (int frame), 'end' (int frame, optional) + blend_modes: Optional list of blend modes per track: + 'REPLACE', 'COMBINE', 'ADD', 'SUBTRACT', 'MULTIPLY' + + Returns: + List of created NLA strips + + Example: + >>> nla_compose(character, [ + ... {'action': walk_action, 'name': 'Walk', 'start': 1}, + ... {'action': wave_action, 'name': 'Wave', 'start': 30} + ... ], blend_modes=['REPLACE', 'ADD']) + """ + if not obj.animation_data: + obj.animation_data_create() + + strips = [] + + for i, action_info in enumerate(actions): + action = action_info['action'] + name = action_info.get('name', action.name) + start = action_info.get('start', 1) + + # Create NLA track + track = obj.animation_data.nla_tracks.new() + track.name = name + + # Push action to track + strip = track.strips.new(name, start, action) + + # Set blend mode + if blend_modes and i < len(blend_modes): + strip.blend_type = blend_modes[i] + + strips.append(strip) + + # Clear active action (NLA takes over) + obj.animation_data.action = None + + return strips + + +def setup_animation_range( + fps: int = 24, + start_frame: int = 1, + end_frame: int = 250, + set_current: bool = True +) -> None: + """ + Configure scene animation settings for production. + + Args: + fps: Frames per second (24=film, 30=video, 60=game) + start_frame: First frame of the animation + end_frame: Last frame of the animation + set_current: Jump playhead to start frame + + Example: + >>> setup_animation_range(fps=30, start_frame=1, end_frame=300) + """ + scene = bpy.context.scene + scene.render.fps = fps + scene.render.fps_base = 1.0 + scene.frame_start = start_frame + scene.frame_end = end_frame + + if set_current: + scene.frame_set(start_frame) + + +def ease_in_out_animation( + obj: bpy.types.Object, + data_path: str, + start_value, + end_value, + start_frame: int = 1, + end_frame: int = 60, + index: int = -1 +) -> None: + """ + Animate a property with smooth ease-in-out interpolation. + Uses Bezier keyframes for natural acceleration/deceleration. + + Args: + obj: Object to animate + data_path: Property path (e.g., 'location', 'scale', 'rotation_euler') + start_value: Starting value + end_value: Ending value + start_frame: First keyframe frame + end_frame: Last keyframe frame + index: Array index (-1 for all components, 0/1/2 for individual) + + Example: + >>> ease_in_out_animation(door, 'rotation_euler', 0, 1.57, index=2) + """ + # Set start value + if index >= 0: + getattr(obj, data_path)[index] = start_value + else: + setattr(obj, data_path, start_value) + obj.keyframe_insert(data_path=data_path, frame=start_frame, index=index) + + # Set end value + if index >= 0: + getattr(obj, data_path)[index] = end_value + else: + setattr(obj, data_path, end_value) + obj.keyframe_insert(data_path=data_path, frame=end_frame, index=index) + + # Set Bezier interpolation (default ease-in-out) + if obj.animation_data and obj.animation_data.action: + for fcurve in obj.animation_data.action.fcurves: + if fcurve.data_path == data_path: + for kf in fcurve.keyframe_points: + kf.interpolation = 'BEZIER' + kf.handle_left_type = 'AUTO_CLAMPED' + kf.handle_right_type = 'AUTO_CLAMPED' diff --git a/data/blender-scripts/tasks/rendering/eevee_setup.py b/data/blender-scripts/tasks/rendering/eevee_setup.py index ae0355a..d24b2e2 100644 --- a/data/blender-scripts/tasks/rendering/eevee_setup.py +++ b/data/blender-scripts/tasks/rendering/eevee_setup.py @@ -5,8 +5,8 @@ "subcategory": "optimization", "tags": ["eevee", "realtime", "optimization", "performance", "rendering"], "difficulty": "intermediate", - "description": "Optimized Eevee render settings for fast previews and final renders.", - "blender_version": "3.0+", + "description": "Optimized Eevee render settings for Blender 5.x fast previews and final renders.", + "blender_version": "5.0+", "estimated_objects": 0 } """ @@ -15,7 +15,14 @@ def setup_eevee_quality(quality: str = 'MEDIUM') -> dict: """ - Configure Eevee quality preset. + Configure Eevee quality preset for Blender 5.x. + + NOTE: In Blender 5.x, many EEVEE properties were removed or changed: + - use_ssr, use_ssr_refraction β€” REMOVED (reflections are automatic) + - use_gtao β€” REMOVED (AO is always active) + - use_bloom β€” REMOVED (use compositor Glare node instead) + - shadow_cascade_size β€” REMOVED + - taa_render_samples β€” REMOVED (use scene.eevee.sampling_render_samples) Args: quality: 'LOW', 'MEDIUM', 'HIGH', 'ULTRA' @@ -24,99 +31,30 @@ def setup_eevee_quality(quality: str = 'MEDIUM') -> dict: Dictionary with applied settings """ bpy.context.scene.render.engine = 'BLENDER_EEVEE' - eevee = bpy.context.scene.eevee presets = { 'LOW': { 'samples': 16, - 'shadows': '512', - 'ssr': False, - 'ao': True, - 'bloom': False }, 'MEDIUM': { - 'samples': 32, - 'shadows': '1024', - 'ssr': True, - 'ao': True, - 'bloom': True + 'samples': 64, }, 'HIGH': { - 'samples': 64, - 'shadows': '2048', - 'ssr': True, - 'ao': True, - 'bloom': True + 'samples': 128, }, 'ULTRA': { - 'samples': 128, - 'shadows': '4096', - 'ssr': True, - 'ao': True, - 'bloom': True + 'samples': 256, } } preset = presets.get(quality, presets['MEDIUM']) - eevee.taa_render_samples = preset['samples'] - eevee.shadow_cascade_size = preset['shadows'] - eevee.use_ssr = preset['ssr'] - eevee.use_gtao = preset['ao'] - eevee.use_bloom = preset['bloom'] + # Blender 5.x: use scene render samples + bpy.context.scene.eevee.taa_samples = preset['samples'] return preset -def enable_eevee_reflections( - quality: str = 'MEDIUM', - thickness: float = 0.5 -) -> None: - """Enable screen-space reflections.""" - eevee = bpy.context.scene.eevee - eevee.use_ssr = True - eevee.use_ssr_refraction = True - eevee.ssr_thickness = thickness - - if quality == 'HIGH': - eevee.ssr_quality = 1.0 - eevee.ssr_max_roughness = 0.5 - else: - eevee.ssr_quality = 0.5 - eevee.ssr_max_roughness = 0.3 - - -def enable_eevee_shadows(soft: bool = True, contact: bool = True) -> None: - """Configure shadow settings.""" - eevee = bpy.context.scene.eevee - eevee.shadow_soft_max = 50 if soft else 0 - eevee.use_shadow_contact = contact - - -def enable_eevee_ambient_occlusion( - distance: float = 0.5, - factor: float = 1.0 -) -> None: - """Enable ambient occlusion.""" - eevee = bpy.context.scene.eevee - eevee.use_gtao = True - eevee.gtao_distance = distance - eevee.gtao_factor = factor - - -def enable_eevee_bloom( - threshold: float = 0.8, - intensity: float = 0.05, - radius: float = 6.5 -) -> None: - """Enable bloom/glow effect.""" - eevee = bpy.context.scene.eevee - eevee.use_bloom = True - eevee.bloom_threshold = threshold - eevee.bloom_intensity = intensity - eevee.bloom_radius = radius - - def add_reflection_probe( location: tuple = (0, 0, 1), influence_distance: float = 2.5, @@ -124,7 +62,10 @@ def add_reflection_probe( name: str = "ReflectionProbe" ) -> bpy.types.Object: """ - Add reflection/light probe. + Add reflection/light probe for EEVEE. + + In Blender 5.x, reflections are handled automatically but probes + can still improve quality for specific areas. Args: location: Probe position @@ -175,6 +116,57 @@ def add_irradiance_volume( return volume +def setup_bloom_compositor( + threshold: float = 0.8, + mix: float = 0.5, + size: int = 8 +) -> None: + """ + Set up bloom/glow effect using the compositor (Blender 5.x). + + In Blender 5.x, eevee.use_bloom is removed. Use a compositor + Glare node instead for bloom/glow effects. + + Args: + threshold: Brightness threshold for bloom + mix: Mix factor (0-1) + size: Glare size (1-9) + """ + scene = bpy.context.scene + scene.use_nodes = True + tree = scene.node_tree + + # Find existing render layers and composite nodes + render_node = None + composite_node = None + for node in tree.nodes: + if node.type == 'R_LAYERS': + render_node = node + elif node.type == 'COMPOSITE': + composite_node = node + + if not render_node or not composite_node: + return + + # Add Glare node for bloom + glare = tree.nodes.new('CompositorNodeGlare') + glare.glare_type = 'BLOOM' + glare.threshold = threshold + glare.mix = mix + glare.size = size + glare.location = (render_node.location.x + 300, render_node.location.y) + + # Rewire: Render Layers -> Glare -> Composite + # Remove existing link from render to composite + for link in tree.links: + if link.to_node == composite_node and link.to_socket.name == 'Image': + tree.links.remove(link) + break + + tree.links.new(render_node.outputs['Image'], glare.inputs['Image']) + tree.links.new(glare.outputs['Image'], composite_node.inputs['Image']) + + def bake_lighting() -> None: """Bake indirect lighting for Eevee.""" bpy.ops.scene.light_cache_bake() @@ -182,7 +174,6 @@ def bake_lighting() -> None: if __name__ == "__main__": setup_eevee_quality('HIGH') - enable_eevee_reflections() - enable_eevee_bloom() + setup_bloom_compositor() - print("Configured Eevee for high quality rendering") + print("Configured Eevee for high quality rendering (Blender 5.x)") diff --git a/data/blender-scripts/tasks/rendering/toon_setup.py b/data/blender-scripts/tasks/rendering/toon_setup.py index f352f28..8838ef3 100644 --- a/data/blender-scripts/tasks/rendering/toon_setup.py +++ b/data/blender-scripts/tasks/rendering/toon_setup.py @@ -6,7 +6,7 @@ "tags": ["npr", "toon", "cartoon", "stylized", "outline", "freestyle"], "difficulty": "intermediate", "description": "Sets up non-photorealistic/toon rendering with outlines.", - "blender_version": "3.0+", + "blender_version": "5.0+", "estimated_objects": 0 } """ @@ -34,6 +34,7 @@ def setup_toon_shading( The created material """ mat = bpy.data.materials.new(name) + mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links @@ -154,17 +155,13 @@ def setup_grease_pencil_outline( def setup_eevee_for_toon() -> None: - """Configure Eevee settings for toon rendering.""" + """Configure Eevee settings for toon rendering (Blender 5.x).""" bpy.context.scene.render.engine = 'BLENDER_EEVEE' + # Note: In Blender 5.x, use_ssr, taa_render_samples, shadow_cascade_size + # are REMOVED. EEVEE handles reflections and shadows automatically. eevee = bpy.context.scene.eevee - eevee.taa_render_samples = 32 - - # Disable reflections/refractions for flat look - eevee.use_ssr = False - - # Simple shadows - eevee.shadow_cascade_size = '1024' + eevee.taa_samples = 32 def create_toon_scene( diff --git a/desktop/assets/modelforge-addon.py b/desktop/assets/modelforge-addon.py index 51746e9..b7e7800 100644 --- a/desktop/assets/modelforge-addon.py +++ b/desktop/assets/modelforge-addon.py @@ -447,19 +447,23 @@ def get_all_object_info(self, max_objects=50, start_index=0): def get_viewport_screenshot(self, max_size=800, filepath=None, format="png"): """ - Capture a screenshot of the current 3D viewport and save it to the specified path. + Capture a screenshot of the current 3D viewport. Parameters: - max_size: Maximum size in pixels for the largest dimension of the image - - filepath: Path where to save the screenshot file + - filepath: Optional path to save the screenshot file. If None, returns + the image as base64-encoded data directly. - format: Image format (png, jpg, etc.) - Returns success/error status + Returns: + - If filepath: {success, width, height, filepath} + - If no filepath: {image (base64), width, height, format} """ - try: - if not filepath: - return {"error": "No filepath provided"} + import os + import tempfile + import base64 + try: # Find the active 3D viewport area = None for a in bpy.context.screen.areas: @@ -470,6 +474,13 @@ def get_viewport_screenshot(self, max_size=800, filepath=None, format="png"): if not area: return {"error": "No 3D viewport found"} + # Determine file path β€” use temp if none provided + return_base64 = filepath is None + if return_base64: + tmp = tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) + filepath = tmp.name + tmp.close() + # Take screenshot with proper context override with bpy.context.temp_override(area=area): bpy.ops.screen.screenshot_area(filepath=filepath) @@ -492,14 +503,36 @@ def get_viewport_screenshot(self, max_size=800, filepath=None, format="png"): # Cleanup Blender image data bpy.data.images.remove(img) - return { - "success": True, - "width": width, - "height": height, - "filepath": filepath - } + if return_base64: + # Read the file and encode as base64 + with open(filepath, "rb") as f: + image_data = base64.b64encode(f.read()).decode("utf-8") + # Clean up temp file + try: + os.remove(filepath) + except OSError: + pass + return { + "image": image_data, + "width": width, + "height": height, + "format": format, + } + else: + return { + "success": True, + "width": width, + "height": height, + "filepath": filepath + } except Exception as e: + # Clean up temp file on error + if return_base64 and filepath: + try: + os.remove(filepath) + except OSError: + pass return {"error": str(e)} def execute_code(self, code): diff --git a/docs/3d-pipeline-integration.md b/docs/3d-pipeline-integration.md new file mode 100644 index 0000000..e66027e --- /dev/null +++ b/docs/3d-pipeline-integration.md @@ -0,0 +1,140 @@ +# ModelForge Pipeline β€” Integration Architecture & Provider APIs + +## The Full Production Pipeline + +``` +USER PROMPT + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 1. GEOMETRY │───▢│ 2. TEXTURE │───▢│ 3. TOPOLOGY β”‚ +β”‚ Base mesh β”‚ β”‚ PBR maps β”‚ β”‚ Clean quads β”‚ +β”‚ Shape/form β”‚ β”‚ UV unwrap β”‚ β”‚ Edge loops β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 4. RIGGING │───▢│ 5. ANIMATION │───▢│ 6. EXPORT β”‚ +β”‚ Skeleton β”‚ β”‚ Keyframes β”‚ β”‚ GLB/FBX/USD β”‚ +β”‚ Weight paint β”‚ β”‚ Motion β”‚ β”‚ Game-ready β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Provider API Integration Plan + +### Tier 1: REST APIs (Integrate Now) + +**Rodin/Hyper3D** β€” Already partially integrated (Rodin job polling exists) +- Endpoint: `https://hyperhuman.deemos.com/api/v2/rodin` +- Modes: Sketch (~20s), Regular (~70s), Gen-2 (~90s) +- Input: text prompt OR image(s), Output: GLB/OBJ/FBX +- Features: 4K textures, adjustable polycount, multi-view, PBR +- Flow: Submit task β†’ poll status β†’ download result β†’ import to Blender + +**Tripo AI** β€” Python SDK available +- Endpoint: `https://api.tripo3d.ai/v2/openapi` +- Full pipeline: generate β†’ retopology β†’ texture β†’ rig β†’ animate +- One-click rigging (unique selling point) +- Export: FBX, OBJ, GLB + +**Meshy AI** β€” REST API +- Text/image to 3D with PBR texturing +- Good for high-fidelity static assets + +### Tier 2: Open Source (Self-Host or HuggingFace) + +**Hunyuan 3D v2** (Tencent) β€” GitHub + HuggingFace Space +- Open-source, can self-host or use free HF space +- Text/image (1-4 photos) to mesh + texture +- AI retopology, rigging built-in + +**TRELLIS** (Microsoft) β€” Open source, ComfyUI integration + +**Meta SAM 3D** β€” Single image to full 3D reconstruction +- Reconstructs shape, texture, AND spatial layout +- Free playground + open source + +### Tier 3: Specialized Tools + +**YVO3D** β€” Texture specialist ($9.99/mo) +**Sparc3D/Hitem3D** β€” Ultra HD mesh ($19.90/mo) +**Spline** β€” Browser 3D editor with AI generation + +## ModelForge's Competitive Advantage + +### What 3D-Agent Does (Our Closest Competitor) +- Text β†’ Blender Python β†’ 3D model (same as us) +- Uses Claude + MCP +- Clean topology claims +- No RAG, no vision, no multi-provider, no scene editing + +### What We Do Better +1. **RAG Pipeline** β€” 127 professional scripts inform every generation +2. **Visual Feedback** β€” Gemini Vision sees viewport, auto-corrects +3. **Scene Editing** β€” Edit existing scenes, not just create new ones +4. **Multi-Step Planning** β€” ReAct planner with per-step validation +5. **LLM Completeness Check** β€” Verifies final scene matches request + +### What Nobody Else Does (Our Moat) +**Multi-Provider Orchestration + Blender Post-Processing** + +``` +User: "Create a game-ready dragon character" + β”‚ + β”œβ”€β–Ά Rodin Gen-2: Generate base mesh from prompt (90s) + β”œβ”€β–Ά Import GLB into Blender via MCP + β”œβ”€β–Ά Blender: Clean topology (decimate, remesh, quads) + β”œβ”€β–Ά YVO3D or Blender: Generate/refine PBR textures + β”œβ”€β–Ά Tripo API: Auto-rig the model + β”œβ”€β–Ά Blender: Weight paint corrections + β”œβ”€β–Ά Blender: Add keyframe animations + β”œβ”€β–Ά Vision: Screenshot β†’ verify quality + └─▢ Export: FBX for Unity/Unreal, GLB for web +``` + +Nobody combines external AI generation with Blender post-processing. + +## Implementation Phases + +### Phase 1: Multi-Provider Generation (Next Sprint) +- [ ] Abstract provider interface: `generate3D(prompt, provider, options)` +- [ ] Integrate Tripo API (full pipeline: generate β†’ rig β†’ animate) +- [ ] Enhance existing Rodin integration with Gen-2 support +- [ ] Add Meshy API for high-fidelity static assets +- [ ] UI: Provider selector in chat (auto-suggest best provider) + +### Phase 2: Pipeline Orchestration +- [ ] Auto-import generated models into Blender scene +- [ ] Blender post-processing: retopology, UV cleanup, material setup +- [ ] Vision validation: Screenshot imported model, verify quality +- [ ] Provider comparison: Generate from 2 providers, let user pick + +### Phase 3: Specialized Integrations +- [ ] Hunyuan 3D HuggingFace API for free-tier users +- [ ] Meta SAM 3D for image-to-3D reconstruction +- [ ] Three.js/code-based generation for parametric shapes +- [ ] Spline-style browser preview before Blender import + +### Phase 4: Full Production Pipeline +- [ ] End-to-end: prompt β†’ mesh β†’ texture β†’ rig β†’ animate β†’ export +- [ ] Preset workflows: "Game asset", "Arch viz", "Animation character" +- [ ] Batch generation: Multiple assets for a scene +- [ ] Version control: Track asset iterations + +## Revenue Model Impact + +| Tier | Current | With Multi-Provider | +|------|---------|-------------------| +| Free | Blender-only generation | + Hunyuan 3D (free), Meta SAM 3D | +| Starter ($12) | Blender + basic RAG | + Rodin Sketch (fast), Tripo basic | +| Pro ($29) | Full RAG + vision | + Rodin Gen-2, Tripo full pipeline, Meshy HD | + +## Sources +- [top3d.ai](https://top3d.ai) β€” Community leaderboard +- [Hyper3D API](https://developer.hyper3d.ai) β€” Rodin REST API docs +- [Tripo API](https://platform.tripo3d.ai) β€” Python SDK + REST +- [Hunyuan 3D](https://github.com/Tencent-Hunyuan/Hunyuan3D-2) β€” Open source +- [Meta SAM 3D](https://ai.meta.com/research/sam3d/) β€” Research + playground +- [3D-Agent](https://3d-agent.com) β€” Direct competitor analysis +- [Spline](https://spline.design) β€” Browser 3D editor diff --git a/docs/3d-pipeline-strategy.md b/docs/3d-pipeline-strategy.md new file mode 100644 index 0000000..e5d54fa --- /dev/null +++ b/docs/3d-pipeline-strategy.md @@ -0,0 +1,168 @@ +# ModelForge β€” 3D Production Pipeline Strategy + +> **Last Updated:** 2026-02-17 +> **Status:** Research Complete β€” Ready for Implementation Planning + +--- + +## Executive Summary + +After deep research across 15+ sources (academic papers, competitor interviews, leaderboards, API docs), we've identified **three distinct approaches** to AI 3D generation. ModelForge's procedural code-gen approach is **academically validated** by the Proc3D paper (SIGGRAPH-track) and is the only approach that produces **editable, parametric, animation-ready** output. This is our moat. + +--- + +## The Three Approaches to AI 3D Generation + +### 1. Neural (Diffusion / NeRF / Gaussian Splatting) +**Used by:** Rodin (CLAY/BANG), Tripo, Meshy, Shap-E, LGM, DreamGaussian + +| Aspect | Details | +|--------|---------| +| **How it works** | Train a 3D-native diffusion model on large 3D datasets. Input: text/image β†’ Output: mesh/point cloud/implicit field | +| **Rodin's approach** | CLAY architecture β€” 3D latent diffusion on 3DShape2VecSet representation. Trained on only 500K 3D assets (vs 7B images for 2D). Won SIGGRAPH 2024 Best Paper Honorable Mention. Gen-2 scales to 10B params (BANG architecture) | +| **Tripo's approach** | Proprietary neural model, image analysis β†’ 3D reconstruction. Offers PBR texture generation | +| **Strengths** | Realistic organic shapes, fast generation (~5-30s), handles arbitrary shapes | +| **Weaknesses** | **Non-editable output** (fused mesh), poor topology, no parametric control, can't animate without cleanup, requires massive training data/compute | +| **Production readiness** | ❌ Requires manual retopology, UV unwrap, rigging β€” NOT animation-ready | + +### 2. Procedural Code-Gen (LLM β†’ Blender/DCC Code) +**Used by:** ModelForge (us!), 3D-Agent, Proc3D, 3D-GPT, SceneCraft, BlenderGPT + +| Aspect | Details | +|--------|---------| +| **How it works** | LLM generates executable code (Python/Blender API) that creates 3D objects procedurally | +| **Proc3D validation** | Introduced PCG (Procedural Compact Graph) β€” 4-10x more compact than raw Blender code. GPT-4o achieves 89% compile rate. Fine-tuned LLaMA-3 reaches 98%. **400x faster edits** vs neural methods | +| **Our approach** | Gemini 2.5 Pro β†’ Blender Python via MCP β†’ 127 RAG scripts β†’ visual feedback loop β†’ auto-correction | +| **Strengths** | **Editable**, parametric, clean topology, animation-ready, runs in Blender (industry standard), low compute | +| **Weaknesses** | Limited to what code can express (hard for organic shapes), requires good prompts, relies on LLM code quality | +| **Production readiness** | βœ… Native Blender output β€” proper topology, UV-ready, riggable | + +### 3. Hybrid (Neural Generation + Procedural Refinement) +**The future approach β€” and ModelForge's opportunity** + +| Aspect | Details | +|--------|---------| +| **How it works** | Use neural models for initial shape/concept, then procedural code for refinement, topology, rigging | +| **Example workflow** | Text β†’ Neural rough shape β†’ Import to Blender β†’ Procedural retopology β†’ UV unwrap β†’ Material assignment β†’ Rig β†’ Animate | +| **Why this wins** | Combines neural creativity with procedural precision | +| **Who's doing this** | Nobody fully yet β€” Rodin CTO says "editing" is their 2026 focus but they're still neural-only | + +--- + +## Competitor Deep Dive + +### Rodin (Hyper3D / Deemos) +- **Tech:** CLAY (3D latent diffusion on 3DShape2VecSet) β†’ BANG (10B param recursive) +- **Key insight from CTO interview:** "We chose the 3D native pipeline... if you use 2D images to recover the 3D surface, it will not be that good" +- **Revenue model:** Free to generate, pay to download +- **2026 focus:** 3D editing β€” upload your model and edit with AI tools +- **Weakness:** Outputs fused meshes, bad topology, NOT game-ready (CTO admits it), no rig/animation +- **Our advantage:** We output native Blender files with proper topology, materials, and can add rigs + +### Tripo AI +- **Tech:** Proprietary neural model, image-to-3D + text-to-3D +- **Has API:** REST API with task-based workflow (create task β†’ poll β†’ download) +- **Pricing:** Credit-based (~$0.10-0.50 per generation) +- **Output:** GLB/FBX/OBJ meshes +- **Weakness:** Single fused mesh, no editing, topology requires cleanup +- **Our advantage:** Full editing workflow inside Blender, RAG-powered code quality + +### 3D-Agent (3d-agent.com) +- **Tech:** Claude AI + Blender MCP (same approach as us!) +- **Closest competitor:** Uses conversational AI to control Blender +- **Weakness:** No RAG pipeline, no fine-tuning, no visual feedback loop, no production workflow +- **Our advantage:** 127 RAG scripts, visual feedback loop, conversation memory, fine-tuning pipeline, full dashboard + +### Spline AI +- **Tech:** Browser-based 3D editor with AI generation +- **Focus:** Web/UI 3D (not production VFX/games) +- **Weakness:** Not Blender-compatible, limited to web export formats +- **Our advantage:** Industry-standard Blender output, full production pipeline + +--- + +## The Full 3D Production Pipeline + +### Idea β†’ Production-Ready Animated Model + +``` +Stage 1: CONCEPT β†’ Text/image/sketch description +Stage 2: GEOMETRY β†’ Base mesh generation (code-gen OR neural import) +Stage 3: TOPOLOGY β†’ Clean quad-based mesh for deformation +Stage 4: UV UNWRAP β†’ Automated UV mapping for texture application +Stage 5: TEXTURING β†’ PBR materials (albedo, roughness, metallic, normal) +Stage 6: RIGGING β†’ Skeleton + weight painting for animation +Stage 7: ANIMATION β†’ Keyframe/procedural animation +Stage 8: RENDERING β†’ Final output (Cycles/EEVEE) +Stage 9: EXPORT β†’ Production formats (FBX, glTF, USD) +``` + +### ModelForge Coverage Today vs. Target + +| Stage | Current | Target | Approach | +|-------|---------|--------|----------| +| Concept | βœ… Text chat | βœ… Text + image + sketch | Add image-to-prompt, sketch-to-prompt | +| Geometry | βœ… Procedural code-gen | βœ… Hybrid (code + neural import) | Add neural model import pipeline | +| Topology | ⚠️ Basic (code-gen makes decent topology) | βœ… Auto retopology | Leverage Blender's Quadriflow/Instant Meshes | +| UV Unwrap | ❌ Not automated | βœ… Smart UV unwrap | Blender's Smart UV Project via MCP | +| Texturing | ⚠️ Basic procedural materials | βœ… PBR + AI textures | Integrate texture gen models (Yvo3D-style) | +| Rigging | ❌ Not implemented | βœ… Auto-rig | Rigify addon (built-in Blender) via MCP | +| Animation | ❌ Not implemented | βœ… AI-driven animation | Keyframe gen via code, motion capture import | +| Rendering | ⚠️ Basic Cycles setup | βœ… Professional presets | RAG scripts already cover this | +| Export | ❌ Not implemented | βœ… Multi-format export | FBX/glTF/USD via Blender export API | + +--- + +## ModelForge's Competitive Advantages + +### Why We Win + +1. **Editable Output** β€” Neural tools produce frozen meshes. We produce parametric, editable Blender files +2. **Full Pipeline** β€” Competitors only do geometry. We can cover conceptβ†’export +3. **RAG-Powered Quality** β€” 127+ professional scripts improve code gen quality. No competitor has this +4. **Visual Feedback Loop** β€” Viewport vision β†’ AI analysis β†’ auto-correction. Unique to us +5. **Industry Standard** β€” Blender is THE open-source DCC tool. Our output works in production pipelines +6. **Hybrid Potential** β€” We can import neural-generated meshes AND refine them procedurally +7. **Fine-Tuning** β€” Custom Qwen3 model for Blender code (269+ training pairs). No competitor has this + +### The Proc3D Validation +The Proc3D paper (Jan 2026, arXiv) directly validates our approach: +- They achieved **89% compile rate** with GPT-4o, **98% with fine-tuned LLaMA-3** +- Their PCG representation is **4-10x more compact** than raw Blender code +- Edits are **400x faster** than neural regeneration methods +- They outperform SDFusion, Shap-E, LLaMA-Mesh on ULIP alignment scores +- **ModelForge already exceeds their capabilities** with RAG, visual feedback, and MCP integration + +--- + +## Revenue Model: Credits + Tiers + +| Tier | Price | Credits/mo | Features | +|------|-------|-----------|----------| +| Free | $0 | 20 credits | Basic geometry gen, 1 project, watermarked export | +| Starter | $12/mo | 200 credits | Full geometry + materials, 10 projects, no watermark | +| Pro | $29/mo | Unlimited | Full pipeline (rig, animate, export), unlimited projects, priority generation, neural import | + +**Credit costs:** 1 credit = 1 generation step. Complex scenes = 5-15 credits. Simple objects = 1-3 credits. + +--- + +## NotebookLM Research Topics + +The following topics should be researched via NotebookLM deep research to build our knowledge base: + +### Priority 1 β€” Core Pipeline Techniques +1. **"Automated 3D retopology techniques Blender Python API"** β€” How to auto-retopologize meshes to clean quads +2. **"Blender Rigify auto-rigging Python scripting"** β€” Automated skeleton + weight painting +3. **"PBR texture generation AI models 2025 2026"** β€” Texture synthesis beyond procedural shaders +4. **"Blender Python keyframe animation procedural generation"** β€” Code-driven animation + +### Priority 2 β€” Neural Integration +5. **"Neural 3D mesh import cleanup Blender workflow"** β€” How to import and clean neural-generated meshes +6. **"3DShape2VecSet latent representation 3D generation"** β€” Understanding Rodin's core representation +7. **"Open source text to 3D models 2025 2026 inference"** β€” Free/open neural models we can self-host + +### Priority 3 β€” Competitive Intelligence +8. **"Production 3D model pipeline game development workflow"** β€” Industry-standard workflows we should replicate +9. **"AI 3D model generation market size competitors 2025 2026"** β€” Market analysis +10. **"Procedural 3D modeling LLM code generation best practices"** β€” Academic literature on our approach diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md new file mode 100644 index 0000000..a19d4df --- /dev/null +++ b/docs/HANDOFF.md @@ -0,0 +1,117 @@ +# ModelForge β€” Session Handoff: 3D Pipeline Implementation + +> **Date:** 2026-02-18 +> **Previous Session:** Deep research + strategy planning for multi-strategy 3D pipeline +> **Next Action:** Begin Phase 1 (RAG Scripts) + Phase 2 (Neural Models on Azure ML) in parallel + +--- + +## What Was Accomplished + +We completed a full research sprint on how to evolve ModelForge from a Blender-only procedural code-gen tool into a **multi-strategy 3D production platform**. Three generation strategies were identified and a 6-phase implementation plan was created. + +### The Three Strategies +1. **Procedural** (existing) β€” LLM generates Blender Python β†’ MCP executes β†’ clean, editable output +2. **Neural** (new) β€” Open-source text/image-to-3D models (Hunyuan 3D, Shap-E, InstantMesh) on Azure ML +3. **Hybrid** (new, THE differentiator) β€” Neural mesh gen β†’ Blender post-processing (retopo β†’ rig β†’ animate β†’ export) + +### Key Decision +**Use ONLY open-source models we can self-host and fine-tune** β€” NOT competitor APIs (Tripo, Meshy, Rodin). This gives us full control over cost, quality, and competitive advantage. + +--- + +## Where All Knowledge Lives + +### Strategy & Research Documents +| File | Contents | +|------|----------| +| `docs/3d-pipeline-strategy.md` | Competitor analysis, 3 approaches mapped, pipeline coverage matrix, revenue model, 10 research topics | +| `docs/research-pipeline-techniques.md` | Concrete Blender Python API code for retopology, auto-rigging (Rigify), PBR textures, procedural animation | +| `GEMINI.md` (Session Log: 2026-02-17) | Full session history, decisions made, files created | + +### Research Sources (NotebookLM) +| Notebook | ID | Sources | Contents | +|----------|----|---------|----------| +| Mastering Blender Automation | `27a1b7ba-0b61-40ca-84e3-f8cce152a724` | 90 | Blender Python API docs, best practices, API pitfalls | +| ModelForge 3D Pipeline Research | `c1ed9929-97af-4c67-944e-ec2932afa0a4` | 28 | Rigify deep research (61 web sources found, 28 imported) | + +### Key Research Findings (from `docs/research-pipeline-techniques.md`) +- **Retopology**: `bpy.ops.object.voxel_remesh()` + `bpy.ops.object.quadriflow_remesh(target_faces=N)` β€” single-line APIs +- **Auto-Rigging**: `rigify.generate.generate_rig(ctx, metarig)` + `bpy.ops.object.parent_set(type='ARMATURE_AUTO')` +- **Animation**: Keyframe insertion, Blender 5.0 `channelbag.fcurves`, NLA strip composition +- **PBR Textures**: No dominant AI solution yet β€” best pattern is AI-generated maps + Principled BSDF loader +- **Blender 4.x/5.0 Migration**: Bone Layers β†’ Bone Collections, action FCurves β†’ channelbag FCurves + +### Existing Codebase Context +| Path | What It Is | +|------|-----------| +| `data/blender-scripts/` | 127 RAG scripts (56 utility + 67 tasks + 4 NotebookLM) | +| `lib/orchestration/planner.ts` | AI planner β€” breaks user requests into steps | +| `lib/orchestration/executor.ts` | Step executor β€” generates + validates + runs Blender code | +| `lib/ai/prompts.ts` | All LLM prompts (planning, code generation, validation) | +| `lib/ai/rag.ts` | Vector similarity search against pgvector | +| `lib/ai/agents.ts` | LangChain agent with RAG + MCP tools | +| `lib/mcp/client.ts` | Blender MCP bridge (socket communication) | +| `lib/ai/vision.ts` | Viewport screenshot analysis via Gemini Vision | +| `training/` | Fine-tuning pipeline: 269 training pairs, QLoRA script, eval prompts | +| `scripts/ingest-blender-docs.ts` | RAG ingestion script (recursive, pgvector) | + +### Open-Source 3D Models to Deploy +| Model | Input | License | Deploy On | +|-------|-------|---------|-----------| +| **Hunyuan 3D 2.0** | Text/Image | Tencent Open | Azure ML A100 | +| **Shap-E** | Text/Image | MIT | Azure ML T4 (cheaper) | +| **InstantMesh** | Single Image | Apache 2.0 | Azure ML A10G | +| **Spark** | Text | Open | Investigate further | + +--- + +## Implementation Plan (6 Phases) + +### Phase 1: RAG Script Creation (START NOW) +Create 6 new scripts in `data/blender-scripts/`: +1. `auto_retopology.py` β€” Voxel remesh + Quadriflow pipeline +2. `auto_rigify.py` β€” Rigify metarig β†’ generate_rig() β†’ auto weight paint +3. `auto_uv_unwrap.py` β€” Smart UV projection +4. `procedural_animation.py` β€” Keyframe math, NLA, orbit/bounce/wave patterns +5. `pbr_texture_loader.py` β€” Load texture maps β†’ Principled BSDF +6. `model_export.py` β€” FBX/glTF/USD/OBJ + LOD generation + +Then: update `lib/ai/prompts.ts` and run `npm run ingest:blender` to re-ingest. + +All API code patterns are in `docs/research-pipeline-techniques.md`. + +### Phase 2: Neural 3D Model Layer (START NOW, parallel with Phase 1) +Create `lib/neural/` β€” abstraction layer for open-source 3D model inference: +- `types.ts` β€” NeuralGenRequest/NeuralGenResult interfaces +- `model-registry.ts` β€” Model catalog + capability mapping +- `hunyuan/client.ts` β€” Azure ML endpoint client for Hunyuan 3D 2.0 +- `shap-e/client.ts` β€” Azure ML endpoint client for Shap-E +- `instant-mesh/client.ts` β€” Azure ML endpoint client for InstantMesh +- `inference/azure-ml.ts` β€” Generic Azure ML managed endpoint caller + +Azure ML setup: deploy models as Managed Online Endpoints (serverless, scale-to-zero). + +### Phase 3: Hybrid Pipeline (after Phase 1 + 2) +Create `lib/orchestration/hybrid-pipeline.ts`: +- Neural gen β†’ download mesh β†’ MCP import to Blender β†’ cleanup β†’ retopo β†’ UV β†’ texture β†’ rig β†’ animate β†’ export +- Create `import_cleanup.py` RAG script for neural mesh import + fix normals/doubles/holes + +### Phase 4: Production Export Presets (after Phase 1) +Game-dev workflow compliance: LODs, normal baking, format presets (Game/VFX/Web/Print). + +### Phase 5: AI Strategy Router (after Phase 2 + 3) +Create `lib/ai/strategy-router.ts` β€” auto-selects procedural vs neural vs hybrid per request. + +### Phase 6: Credit System (after Phase 5) +Stripe metered billing per generation step. Tier pricing: Free ($0/20 credits) β†’ Starter ($12/200) β†’ Pro ($29/unlimited). + +--- + +## Full Implementation Plan (Detailed) +See: `C:\Users\krist\.gemini\antigravity\brain\00537199-3b24-4f15-b798-38b6cb9e861c\implementation_plan.md` + +## Git Status +- Branch: `main`, 8 commits ahead of origin +- Working tree: clean +- All research docs committed diff --git a/docs/addon-integration-roadmap.md b/docs/addon-integration-roadmap.md new file mode 100644 index 0000000..63c89a3 --- /dev/null +++ b/docs/addon-integration-roadmap.md @@ -0,0 +1,146 @@ +# Blender Addon Integration Roadmap + +> **Last Updated:** 2026-02-17 +> **Source:** BlenderHub.net (top 50, free, geometry nodes, animation, rendering pages) +> **Purpose:** Track addons ModelForge could integrate via MCP `execute_code` + +--- + +## How Integration Works + +Our MCP server executes arbitrary Python inside Blender, so we can: +- Call any installed addon's operators: `bpy.ops.addon_name.function()` +- Import addon modules: `import addon_module` +- Access built-in addons (Rigify, Node Wrangler, etc.) β€” no install needed + +--- + +## 🟒 Priority 1 β€” Built-in Addons (Zero Install) + +These ship with Blender and just need enabling via `bpy.ops.preferences.addon_enable()`. + +| Addon | Category | Integration Value | Python API | +|-------|----------|-------------------|------------| +| **Node Wrangler** | Shading | Auto-connect texture nodes (Ctrl+T equivalent) | `bpy.ops.node.nw_*` | +| **Rigify** | Rigging | One-click character rigs from meta-rigs | `bpy.ops.pose.rigify_*` | +| **Import Images as Planes** | Import | Quick image/texture plane creation | `bpy.ops.import_image.to_plane()` | +| **Cell Fracture** | Effects | Shatter objects for destruction effects | `bpy.ops.object.add_fracture_cell_objects()` | +| **BoltFactory** | Modeling | Procedural nuts, bolts, screws | `bpy.ops.mesh.bolt_add()` | +| **Archimesh** | Architecture | Doors, walls, windows, furniture | `bpy.ops.mesh.archimesh_*` | +| **Extra Mesh Objects** | Modeling | Gears, gems, pipes, torii, etc. | `bpy.ops.mesh.primitive_*` | +| **Extra Curve Objects** | Modeling | Spirals, knots, profiles | `bpy.ops.curve.spirals_add()` | +| **A.N.T. Landscape** | Terrain | Procedural terrains and landscapes | `bpy.ops.mesh.landscape_add()` | +| **Sapling Tree Gen** | Nature | Procedural trees | `bpy.ops.curve.tree_add()` | +| **IvyGen** | Nature | Procedural ivy growth on surfaces | `bpy.ops.curve.ivy_add()` | + +--- + +## 🟑 Priority 2 β€” Free Addons (High Value) + +| Addon | Category | Price | What It Does | URL | +|-------|----------|-------|-------------|-----| +| **EasyBPY** | API | Free | Simplifies bpy into readable functions | [Gumroad](https://curtisjamesholt.gumroad.com/l/easybpy) | +| **ND (Non-Destructive)** | Hard Surface | Free | Non-destructive hard surface modeling | [Gumroad](https://hugemenace.gumroad.com/l/nd-blender-addon) | +| **Realtime Materials** | Materials | Free | Huge procedural material library (Cycles+EEVEE) | [BlenderMarket](https://blendermarket.com/products/realtime-materials-for-blender) | +| **BY-GEN** | Procedural | Free | Procedural generative structures | [Gumroad](https://curtisjamesholt.gumroad.com/l/BY-GEN) | +| **Nature Clicker** | Scattering | Free | Click-to-place objects with instancing | [Gumroad](https://oliverjpost.gumroad.com/l/QfghQ) | +| **Kit Ops 3 Free** | Kitbashing | Free | Insert objects with booleans | [Gumroad](https://chippwalters.gumroad.com/l/kitops3free) | +| **Geo Cables** | Cables | Free | Draw/click cables on surfaces | [Gumroad](https://amanbairwal.gumroad.com/l/GeoCables) | +| **Lightning Bolts** | Effects | Free | Procedural lightning via geometry nodes | [Gumroad](https://bbbn19.gumroad.com/l/jCoDy) | +| **Lego Assembly** | Fun | Free | Dynamic Lego assembly via geo nodes | [Gumroad](https://bbbn19.gumroad.com/l/dIQLD) | +| **Holt Tools** | Utility | Free | .blend cleanup, mesh optimization | [Gumroad](https://curtisjamesholt.gumroad.com/l/holt-tools) | +| **BlenderKit** | Assets | Free+ | Cloud asset library inside Blender | [Website](https://www.blenderkit.com/get-blenderkit/) | +| **Blender GIS** | Mapping | Free | Import satellite terrain + buildings | [GitHub](https://github.com/domlysz/BlenderGIS) | +| **BagaPie** | Geo Nodes | Free | Modifier presets for scattering, arrays | [Gumroad](https://abaga.gumroad.com/l/BbGVh) | +| **Camera Shakify** | Animation | Free | Realistic camera shake from tracking data | [GitHub](https://github.com/EatTheFuture/camera_shakify) | +| **Wiggle Bones** | Animation | Free | Follow-through physics on bones | [BlenderArtists](https://blenderartists.org/t/wiggle-2/1454788) | +| **BoneDynamics** | Animation | Free+ | Physics simulation on bones | [BlenderMarket](https://blendermarket.com/products/bonedynamics) | +| **Dynamic Parent** | Animation | Free | Switch bone parents via keyframes | [GitHub](https://github.com/romanvolodin/dynamic_parent) | +| **Pidgeon Toolbag** | Rendering | Free | Super denoiser + fast render + AI upscale | [Gumroad](https://pidgeontools.gumroad.com/l/PidgeonToolBag) | +| **Post FX** | Compositing | Free+ | Advanced compositing nodes (flares, distortion) | [BlenderMarket](https://blendermarket.com/products/post-fx) | +| **Alt Tab Easy Fog** | Effects | Free | 50+ volume presets for fog scenes | [BlenderMarket](https://blendermarket.com/products/alt-tab-easy-fog2) | +| **Alt Tab Ocean** | Effects | Free | Ocean generator with caustics | [BlenderMarket](https://blendermarket.com/products/alt-tab-water) | +| **Import As Decal** | Texturing | Free | Convert images to decals/stickers | [Gumroad](https://amanbairwal.gumroad.com/l/ImportAsDecal) | +| **Erode It** | Effects | Free+ | Procedural damage with one button | [BlenderMarket](https://blendermarket.com/products/erode-it) | +| **Malt Render** | Rendering | Free | Customizable realtime NPR render engine | [Website](https://malt3d.com/) | +| **Ragdoll Dynamics** | Physics | Free | Easy ragdoll rigs combined with animations | [BlenderMarket](https://blendermarket.com/products/ragdoll-dynamics) | + +--- + +## πŸ”΄ Priority 3 β€” Paid Addons (Future Consideration) + +### Animation & Rigging +| Addon | Price | Integration Value | +|-------|-------|-------------------| +| **Auto-Rig Pro** | $25+ | Automatic full-body rigging with IK | +| **Faceit** | $78+ | Face rigging, animation, motion capture | +| **X-Muscle System** | $35+ | Realistic muscle/skin deformation | +| **Animation Layers** | $28 | Layer-based NLA workflow | +| **Animax** | $35 | Procedural multi-object animation | + +### Modeling & Hard Surface +| Addon | Price | Integration Value | +|-------|-------|-------------------| +| **Hard Ops + Boxcutter** | $38 | Fast boolean hard-surface modeling | +| **MESHmachine** | $45 | Chamfer/bevel manipulation | +| **Fluent: Power Trip** | $30 | Non-destructive boolean workflow | +| **Cablerator** | $14+ | Auto-generate cables between surfaces | +| **Mechanical Creature Kit** | Free+ | Procedural bolts, cogs, mechanisms | + +### Materials & Texturing +| Addon | Price | Integration Value | +|-------|-------|-------------------| +| **Extreme PBR Nexus** | $39+ | 1800+ materials, PBR texture paint | +| **SimpleBake** | $20 | One-click PBR texture baking | + +### Rendering & Compositing +| Addon | Price | Integration Value | +|-------|-------|-------------------| +| **K-Cycles** | $54+ | Faster Cycles with bloom + lens flares | +| **Physical Starlight** | $70+ | Procedural atmosphere, stars, clouds | +| **Cam-FX** | $30 | Anamorphic lens dirt and bokeh | +| **Light Wrangler** | $14+ | Quick professional lighting setups | + +### Procedural & Geometry Nodes +| Addon | Price | Integration Value | +|-------|-------|-------------------| +| **Flip Fluids** | $76 | High-quality liquid simulation | +| **Procedural Crowds** | $18+ | Photo-scanned animated characters | +| **Procedural Alleys** | $18+ | Alley scenes via geometry nodes | +| **Curves to Roads** | $5 | Geometry nodes road generation | +| **Realtime Flame** | $10 | Real-time fire for Cycles/EEVEE | +| **Droplet Generator** | $15 | Procedural condensation | +| **Pipe Systems** | $6+ | Auto-generate piping around meshes | +| **botaniq** | $2+ | Massive nature asset library | + +--- + +## πŸ—οΈ Integration Strategy + +### Phase 1: Built-in Addons (No dependencies) +1. Enable addons via `bpy.ops.preferences.addon_enable(module="addon_name")` +2. Create RAG scripts for each built-in addon's API +3. Update CODE_GENERATION_PROMPT with built-in addon guidance +4. Test: "Create a landscape with trees" β†’ A.N.T. Landscape + Sapling + +### Phase 2: Free Community Addons +1. Document installation requirements in user setup guide +2. Create detection scripts: check if addon is installed before using +3. Fallback: if addon not installed, use vanilla bpy equivalent +4. Priority targets: Realtime Materials, BY-GEN, Nature Clicker, Geo Cables + +### Phase 3: Paid Addons (Premium feature) +1. Detect installed paid addons and expose their capabilities +2. Update planner to suggest addon-powered approaches when available +3. Create addon-specific RAG scripts for popular paid addons + +--- + +## πŸ“š Sources + +- [Top 50 Blender Addons (BlenderHub)](https://blenderhub.net/top-free-and-paid-blender-addons/) +- [Top 20 Free Addons (BlenderHub)](https://blenderhub.net/free-blender-addons/) +- [Top 35 Geometry Node Addons (BlenderHub)](https://blenderhub.net/geometry-node-addons/) +- [Top 15 Animation Addons (BlenderHub)](https://blenderhub.net/top-15-animation-addons-for-blender/) +- [Top 20 Rendering Addons (BlenderHub)](https://blenderhub.net/top-10-rendering-addons-for-blender/) +- [Duet PBR (CC0 textures)](https://www.duetpbr.com/) diff --git a/docs/research-pipeline-techniques.md b/docs/research-pipeline-techniques.md new file mode 100644 index 0000000..4bf4ceb --- /dev/null +++ b/docs/research-pipeline-techniques.md @@ -0,0 +1,272 @@ +# ModelForge β€” 3D Pipeline Technique Research + +> **Generated:** 2026-02-17 +> **Sources:** NotebookLM Deep Research (61 web sources + 90 existing notebook sources) + +--- + +## 1. Automated Retopology (Blender Python API) + +### Voxel Remesh +The fastest automated retopology method. Creates a unified, watertight mesh from complex geometry. + +```python +import bpy + +obj = bpy.context.active_object +if obj.type == 'MESH': + # Smaller value = more detail (0.003 for characters, 0.03 for environments) + obj.data.remesh_voxel_size = 0.03 + bpy.ops.object.mode_set(mode='SCULPT') + bpy.ops.object.voxel_remesh() + bpy.ops.object.mode_set(mode='OBJECT') +``` + +### Quadriflow Remesh +Clean quad-based topology β€” essential for animation deformation. + +```python +bpy.ops.object.quadriflow_remesh( + target_faces=5000, # Desired face count + use_mesh_symmetry=True, # Enable X-axis symmetry + preserve_sharp=True # Keep sharp edges +) +``` + +### Full Retopology Pipeline +1. **Cleanup & Merge**: `bpy.ops.object.join()` to combine parts +2. **Voxel Remesh**: Unify geometry, remove internal faces +3. **Decimate** (optional): `bpy.ops.object.modifier_add(type='DECIMATE')` to reduce vertex count before expensive retopo +4. **Quadriflow**: Generate final clean quad topology + +> **Key Principle**: Prefer `bpy.data` over `bpy.context` for robust automation. Operators (`bpy.ops`) require correct context (active object). + +--- + +## 2. Auto-Rigging with Rigify (Deep Research β€” 61 Sources) + +### Core Architecture +Rigify operates on a **metarig β†’ generate β†’ skin** pipeline: +- **Metarig**: Structural template with bone metadata (`rigify_type` attributes) +- **Generation**: Recursive traversal creates MCH- (mechanism), ORG- (original), DEF- (deformation) bones +- **Skinning**: Mesh binding via automatic weights to DEF- bones only + +### Programmatic Rig Generation + +```python +import rigify +import bpy + +# Step 1: Create metarig +bpy.ops.object.armature_add() +bpy.ops.object.mode_set(mode='EDIT') +bpy.ops.armature.metarig_sample_add() + +# Step 2: Align metarig bones to mesh (vertex group centers) +# Calculate joint positions from mesh vertex data +# joint_pos = mean(vertex_positions_in_group) + +# Step 3: Generate the rig +metarig = bpy.data.objects['metarig'] +rigify.generate.generate_rig(bpy.context, metarig) + +# Step 4: Bind mesh to rig +# Select mesh first, then rig +bpy.ops.object.parent_set(type='ARMATURE_AUTO') +``` + +### Available Templates + +| Template | Use Case | +|----------|----------| +| `Human` | Full facial rig, 5 fingers, complex spine | +| `Basic Human` | Biped limbs + spine only (no face/fingers) | +| `Basic Quadruped` | Animals (4 legs, tail, spine) | +| `Cat`, `Wolf`, `Horse`, `Bird`, `Shark` | Archetype-specific features | + +### Custom Rig Types +- Assign via: `pose_bone.rigify_type = 'spines.basic_tail'` +- Configure via: `pose_bone.rigify_parameters` +- Custom Feature Sets: Inherit from `rigify.base_rig.BaseRig` + +### Blender 4.x/5.0 Migration Notes +- **Bone Layers β†’ Bone Collections**: Use `rigify_ui_row` and `rigify_ui_title` properties +- **Bone Visibility**: `pose.bones.hide` (not `bones.hide`) for Object/Pose mode +- **Bone Selection**: Now stored on `pose_bones.select` +- **Action API**: Legacy `action.fcurves` removed β†’ use `channelbag.fcurves` + +### Automated Weight Painting +```python +# For deforming meshes (organic characters) +bpy.ops.object.parent_set(type='ARMATURE_AUTO') + +# For rigid parts (robots, accessories) +bpy.ops.object.parent_set(type='ARMATURE_NAME') # Empty groups +# Then assign 100% weight to specific bones: +v_group = mesh_obj.vertex_groups.get(bone_name) +v_group.add([vertex_index], 1.0, 'REPLACE') +``` + +### Common Pitfalls +1. **Disjoint bones**: Tail of bone N must match head of bone N+1 +2. **Auto Run Python Scripts**: Must be enabled for `rig_ui.py` +3. **Non-manifold geometry**: Causes auto-weights to fail +4. **Scale**: Rigify expects 1 unit = 1 meter (apply transforms first) + +--- + +## 3. PBR Texture Generation + +### Current State of AI Texturing (2025-2026) + +| Tool | Approach | Status | +|------|----------|--------| +| **TEXTure** | Text-guided texture synthesis with depth-conditioned diffusion | Open-source | +| **Text2Tex** | Text-to-texture using depth maps + iterative inpainting | Open-source | +| **ControlNet Depth** | SD + depth map conditioning for UV-aware texture gen | Open-source | +| **Meshy AI** | Commercial text-to-texture API | Paid API | +| **Yvo3D** | Experimental AI texturing | Research | +| **Stable Diffusion + UV Projection** | Generate 2D textures β†’ project onto UV maps | DIY | + +### Blender Integration Pattern +```python +# Load PBR texture maps from AI generation +import bpy + +def apply_pbr_textures(obj, albedo_path, roughness_path, metallic_path, normal_path): + mat = bpy.data.materials.new(name="AI_PBR_Material") + mat.use_nodes = True + nodes = mat.node_tree.nodes + links = mat.node_tree.links + + bsdf = nodes.get("Principled BSDF") + + # Albedo + albedo_tex = nodes.new('ShaderNodeTexImage') + albedo_tex.image = bpy.data.images.load(albedo_path) + links.new(albedo_tex.outputs['Color'], bsdf.inputs['Base Color']) + + # Roughness + rough_tex = nodes.new('ShaderNodeTexImage') + rough_tex.image = bpy.data.images.load(roughness_path) + rough_tex.image.colorspace_settings.name = 'Non-Color' + links.new(rough_tex.outputs['Color'], bsdf.inputs['Roughness']) + + # Normal Map + normal_tex = nodes.new('ShaderNodeTexImage') + normal_tex.image = bpy.data.images.load(normal_path) + normal_tex.image.colorspace_settings.name = 'Non-Color' + normal_map = nodes.new('ShaderNodeNormalMap') + links.new(normal_tex.outputs['Color'], normal_map.inputs['Color']) + links.new(normal_map.outputs['Normal'], bsdf.inputs['Normal']) + + obj.data.materials.append(mat) +``` + +### Smart UV Unwrap Automation +```python +# Auto-UV unwrap before texture application +bpy.ops.object.mode_set(mode='EDIT') +bpy.ops.mesh.select_all(action='SELECT') +bpy.ops.uv.smart_project( + angle_limit=66.0, + island_margin=0.02, + area_weight=0.0 +) +bpy.ops.object.mode_set(mode='OBJECT') +``` + +--- + +## 4. Procedural Keyframe Animation (Blender Python API) + +### Basic Keyframe Insertion +```python +import bpy + +# Animate object location +obj = bpy.context.active_object +obj.location.y = 0 +obj.keyframe_insert("location", frame=1) +obj.location.y = 5.0 +obj.keyframe_insert("location", frame=60) +``` + +### Procedural Math-Driven Animation +```python +import math + +obj = bpy.data.objects['Cube'] +for frame in range(1, 101): + bpy.context.scene.frame_set(frame) + # Sine wave bobbing + obj.location.z = math.sin(frame * 0.1) * 0.5 + obj.keyframe_insert(data_path="location", index=2, frame=frame) + + # Rotation + obj.rotation_euler.z = frame * 0.05 + obj.keyframe_insert(data_path="rotation_euler", index=2, frame=frame) +``` + +### Constraints for Physics-Like Animation +```python +# Track To constraint (camera follows target) +constraint = camera.constraints.new(type='TRACK_TO') +constraint.target = target_object +constraint.track_axis = 'TRACK_NEGATIVE_Z' +constraint.up_axis = 'UP_Y' +``` + +### F-Curve Manipulation (Blender 5.0+) +```python +import bpy_extras + +# Modern API: F-curves stored in Channelbags +action = obj.animation_data.action +slot = action.slots[0] +channelbag = bpy_extras.anim_utils.action_ensure_channelbag_for_slot(action, slot) + +# Create F-curve in channelbag +fcurve = channelbag.fcurves.new( + data_path="location", + index=0, + group_name="Object Transforms" +) +``` + +### NLA Strip Composition +```python +# Push action to NLA stack for layering +anim_data = obj.animation_data +track = anim_data.nla_tracks.new() +track.name = "Base Walk Cycle" +strip = track.strips.new("walk", start=1, action=action) + +# Add noise layer on top +noise_track = anim_data.nla_tracks.new() +noise_track.name = "Procedural Noise" +noise_strip = noise_track.strips.new("noise", start=1, action=noise_action) +noise_strip.blend_type = 'ADD' # Layer on top +``` + +--- + +## Key Takeaways for ModelForge Implementation + +### What We Can Build NOW +1. **Retopology Pipeline**: Voxel Remesh + Quadriflow are both single-line API calls β€” add to MCP +2. **Auto-Rigging**: Rigify `generate_rig()` is well-documented. Add human/quadruped templates +3. **Smart UV Unwrap**: `bpy.ops.uv.smart_project()` β€” trivial to add +4. **Procedural Animation**: Keyframe insertion + math-driven motion β€” extend our code-gen prompts + +### What Needs More Research +1. **AI Texture Generation**: No single dominant solution yet. Best bet: SD + ControlNet depth-guided +2. **Mesh-to-Rig Alignment**: Automated bone placement needs vertex group analysis +3. **Motion Library**: Pre-built animation patterns (walk, run, idle) as RAG scripts + +### Recommended RAG Scripts to Create +1. `auto_retopology.py` β€” Voxel remesh + Quadriflow pipeline +2. `auto_rigify.py` β€” Rigify metarig generation + skinning +3. `auto_uv_unwrap.py` β€” Smart UV projection automation +4. `procedural_animation.py` β€” Math-driven keyframe patterns +5. `pbr_texture_loader.py` β€” Load and apply PBR texture maps diff --git a/lib/ai/index.ts b/lib/ai/index.ts index fc44af4..ccaf0cb 100644 --- a/lib/ai/index.ts +++ b/lib/ai/index.ts @@ -15,7 +15,7 @@ import OpenAI from "openai" // Configuration // ============================================================================ -const DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-2.5-pro" +const DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-3.1-pro-preview" const EMBEDDING_MODEL = "togethercomputer/m2-bert-80M-32k-retrieval" const EMBEDDING_DIMENSIONS = 768 diff --git a/lib/ai/prompts.ts b/lib/ai/prompts.ts index 0b17c1f..ac41ec7 100644 --- a/lib/ai/prompts.ts +++ b/lib/ai/prompts.ts @@ -10,20 +10,10 @@ import { ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemp // System Prompts // ============================================================================ -export const BLENDER_SYSTEM_PROMPT = `You are ModelForge, an AI assistant that orchestrates Blender through the Model Context Protocol (MCP). +export const BLENDER_SYSTEM_PROMPT = `You are ModelForge, an AI assistant that orchestrates Blender 5.x through the Model Context Protocol (MCP). -Your responsibilities: -1. Generate Python scripts for Blender operations -2. Plan multi-step scene construction -3. Validate and recover from errors -4. Apply materials and styling appropriately - -Guidelines: -- Use bpy module for all Blender operations -- Keep scripts focused and idempotent (safe to rerun) -- Apply materials in the same step that creates geometry -- Always ensure scenes have lighting and camera unless explicitly forbidden -- Break complex requests into component steps` +You plan multi-step scene construction, generate Python scripts, validate outcomes, and recover from errors. +Target Blender version: 5.0+ (users are required to run Blender 5 or above).` export const PLANNING_SYSTEM_PROMPT = `You are ModelForge's orchestration planner. You produce a JSON plan that a separate executor will carry out step-by-step against Blender via MCP tools. @@ -37,11 +27,16 @@ PLANNING PRINCIPLES: 2. Decompose complex objects into sub-components (e.g., "castle" β†’ walls, towers, roof, door, windows, courtyard). 3. Each step must accomplish ONE clear objective β€” don't combine unrelated operations. 4. Materials, colors, and shading MUST be applied in the SAME execute_code step that creates the geometry β€” never as a separate step. -5. Plan order: inspect β†’ delete/clear β†’ create geometry (with materials) β†’ lighting β†’ camera. +5. Plan order depends on the request type: + - NEW SCENE: inspect β†’ clear default objects β†’ create geometry (with materials) β†’ lighting β†’ camera. + - EDIT SCENE: inspect β†’ modify/add only what the user asked for. NEVER delete objects the user didn't mention. Preserve existing lights and camera. When placing objects ON or NEAR existing objects, reference the EXACT object name and location from get_scene_info in your step description (e.g. "Place 'Hot_Sword' on top of 'Anvil' at its location (0.0, 0.5, 0.8)"). NEVER recreate objects that already exist in the scene β€” use their names and known positions. 6. Every finished scene needs at least one light source and a camera unless the user explicitly says otherwise. 7. Use descriptive object names (e.g., "Castle_Tower_Left") so downstream steps can reference them. 8. Prefer fewer, well-described execute_code steps over many tiny ones β€” each one has overhead. 9. NEVER plan boolean operations for simple architectural details (doors, windows, arches). Instead, describe them as separate geometry placed at the surface. Booleans are fragile and often destroy meshes. +10. When EDITING an existing scene, NEVER delete existing lights unless the user explicitly asks to remove them. If adding new light sources (candles, lamps, etc.), keep the existing scene lighting. Scenes without adequate lighting appear completely black in rendered view. +11. OBJECT GROUNDING: When describing objects that rest on surfaces (floor, walls, tables), ALWAYS specify their exact Z position so they don't float. Objects on the floor must have Z=0 (or Z=half_height for centered origins). Wall-mounted objects (racks, shelves, paintings) must specify their world-space position flush against the wall surface, not floating in mid-air. Include explicit coordinates in your description. +12. LIGHTING ENERGY: Point/Spot/Area lights need high energy to illuminate indoor scenes. Use at minimum: Point lights 500-1000W, Area lights 300-800W, Sun lights 3-5 W/mΒ². Darker scenes (forges, caves) need at least ONE strong point light (1000W+) and ONE fill light (300W+). Scenes that are too dark in rendered view are a failure. CRITICAL RULES FOR execute_code STEPS: - NEVER put Python code in the parameters. @@ -82,8 +77,9 @@ STRICT RULES: 6. Output ONLY raw Python code β€” no markdown fences, no explanations, no comments about what the code does. COMMON PATTERNS: -- Create a material (Blender 5.x β€” use_nodes is auto-enabled): +- Create a material: mat = bpy.data.materials.new(name='MyMaterial') + mat.use_nodes = True # Safe on all Blender versions bsdf = mat.node_tree.nodes.get('Principled BSDF') bsdf.inputs['Base Color'].default_value = (R, G, B, 1.0) bsdf.inputs['Roughness'].default_value = 0.5 @@ -98,16 +94,15 @@ COMMON PATTERNS: - Set active camera: bpy.context.scene.camera = cam_obj -BLENDER 5.x API β€” CRITICAL: -- \`material.use_nodes = True\` is DEPRECATED in Blender 5.0+ β€” do NOT call it. The node tree is auto-created by \`bpy.data.materials.new()\`. -- \`world.use_nodes = True\` is also DEPRECATED β€” same auto-creation behavior. -- The EEVEE render engine identifier is now "BLENDER_EEVEE" (not "BLENDER_EEVEE_NEXT"). -- The Principled BSDF shader (since Blender 4.0) RENAMED several inputs: - β€’ "Specular" is now "Specular IOR Level" (or just skip it β€” default is fine) - β€’ "Emission" was SPLIT into "Emission Color" and "Emission Strength" - β€’ "Transmission" is now "Transmission Weight" +BLENDER 5.x API: +- ALWAYS call \`mat.use_nodes = True\` after creating a material before accessing \`mat.node_tree\`. +- ALWAYS call \`world.use_nodes = True\` before accessing world node tree. +- The EEVEE render engine identifier is "BLENDER_EEVEE". +- Principled BSDF socket names (Blender 5.x): + β€’ "Specular IOR Level" (not "Specular") + β€’ "Emission Color" + "Emission Strength" (not just "Emission") + β€’ "Transmission Weight" (not "Transmission") β€’ Always use .get() to access shader inputs safely: bsdf.inputs.get('Metallic') - β€’ "Metallic" is still "Metallic" (not "Metalic" β€” watch the spelling) - For emission/glow effects, set BOTH: bsdf.inputs['Emission Color'].default_value = (R, G, B, 1.0) bsdf.inputs['Emission Strength'].default_value = 5.0 @@ -133,7 +128,7 @@ MESH SAFETY β€” ALWAYS VALIDATE: - For NumPy foreach_set: ALWAYS .flatten() the array before passing to Blender. LIGHT UNITS β€” CRITICAL: -- Point, Spot, Area lights: energy in WATTS (e.g., 500W for key light) +- Point, Spot, Area lights: energy in WATTS (e.g., 1000W for key light, minimum 500W) - Sun lights: energy in WATTS/mΒ² β€” use 3-10 W/mΒ² for typical scenes. NEVER set sun to 1000! - Light color is 3-tuple RGB: light.color = (1.0, 0.0, 0.0). NOT 4-tuple RGBA! - For soft shadows, increase area light size: light_data.size = 2.0 @@ -161,22 +156,31 @@ VOLUMETRIC EFFECTS: - Keep density very low: 0.001-0.005 for atmosphere, 0.02-0.05 for fog. AVOID: -- Calling \`mat.use_nodes = True\` β€” deprecated in Blender 5.x, node tree is auto-created. - Using deprecated \`bpy.context.scene.objects.link()\` β€” use \`bpy.context.collection.objects.link()\` if needed. - Hard-coding absolute file paths. - Calling \`bpy.ops\` operators that require specific UI context without overriding context. - Accessing \`bpy.context.active_object\` after deleting objects β€” it may be None or stale. - Use \`bpy.data.objects.remove(obj, do_unlink=True)\` to delete, then re-fetch references. - dict-style property access on API objects (removed in 5.0): scene['cycles'] β†’ use scene.cycles - -BOOLEAN OPERATIONS β€” CRITICAL (Blender 5.x): -- The ONLY valid solvers are: 'EXACT', 'FLOAT', 'MANIFOLD'. -- NEVER use 'FAST' β€” it does NOT exist and will crash. -- Always use solver='EXACT' for reliable results: \`bool_mod.solver = 'EXACT'\` -- PREFER avoiding boolean operations entirely for low-poly/simple models. - Instead of boolean cuts for doors/windows, use separate geometry placed at the wall surface, - or use inset/extrude approaches. Booleans are fragile and can destroy the target mesh. -- If you must use a boolean, always clean up the cutter object after applying: +- REMOVED SHADER SOCKETS (will crash with 'key not found'): + β€’ 'Subsurface Color' β€” REMOVED. Base Color drives SSS color directly. + β€’ 'Specular' β€” renamed to 'Specular IOR Level'. + β€’ 'Transmission' β€” renamed to 'Transmission Weight'. + β€’ 'Emission' β€” split into 'Emission Color' and 'Emission Strength'. + β€’ 'Subsurface' β€” renamed to 'Subsurface Weight'. + Always use .get() to access sockets safely and handle None. +- REMOVED MATERIAL ATTRIBUTES (will crash β€” NEVER use these): + β€’ mat.shadow_method β€” DOES NOT EXIST. Do NOT use. Do NOT try alternatives. + β€’ mat.shadow_mode β€” DOES NOT EXIST. Do NOT use. Do NOT try alternatives. + For transparent/alpha materials, ONLY set: mat.blend_method = 'BLEND' + Valid blend_method values: 'OPAQUE', 'CLIP', 'HASHED', 'BLEND'. NOTHING ELSE (NOT 'ALPHA_BLEND'). + β€’ eevee.use_ssr / eevee.use_ssr_refraction / eevee.use_screen_space_reflections β€” ALL REMOVED in Blender 5.x. EEVEE handles reflections automatically. Do NOT access SceneEEVEE properties for SSR. + +BOOLEAN OPERATIONS: +- The ONLY valid solvers are: 'EXACT', 'FLOAT', 'MANIFOLD'. NEVER use 'FAST'. +- Always use solver='EXACT' for reliable results. +- PREFER avoiding boolean operations for low-poly/simple models β€” use separate geometry instead. +- If you must use a boolean, apply and clean up the cutter: \`bpy.ops.object.modifier_apply(modifier=mod.name)\` \`bpy.data.objects.remove(cutter, do_unlink=True)\` @@ -211,6 +215,27 @@ PROCEDURAL TEXTURES β€” BEST PRACTICES: between clean and damaged material properties (color, roughness, bump). - Use ShaderNodeTexCoord β†’ ShaderNodeMapping β†’ Texture for full control over UV scaling. +PRODUCTION PIPELINE β€” AVAILABLE CAPABILITIES: +- RETOPOLOGY: Use voxel_remesh() + quadriflow_remesh() for cleaning neural/sculpted meshes. +- RIGGING: Rigify addon β€” create_metarig() templates (human/quadruped/bird) + generate_rig() + auto weight paint. +- UV UNWRAP: auto_uv_pipeline() detects best method per shape. lightmap_uv() for bake UVs. +- ANIMATION: orbit_animation(), wave_animation(), pendulum_animation(), spring_animation(), NLA composition. +- PBR TEXTURES: apply_pbr_textures() loads albedo/roughness/metallic/normal/AO maps β†’ Principled BSDF. +- EXPORT: generate_lods() for LOD chains. export_with_preset('game'|'vfx'|'web'|'print'). USD support via export_usd(). +(Detailed code patterns are available in RAG β€” the AI will retrieve relevant scripts automatically.) + +NEURAL 3D GENERATION β€” WHEN TO USE: +- PREFER PROCEDURAL (Blender Python) for: architectural objects, geometric shapes, furniture, + parametric designs, anything with clean edges and precise dimensions. +- PREFER NEURAL (AI generation) for: organic characters, animals, plants, complex sculptures, + photorealistic assets that are hard to model procedurally. +- HYBRID PIPELINE: Neural geometry β†’ Blender retopology β†’ Neural or Blender texturing β†’ Blender rigging/animation/export. +- Neural providers available: Hunyuan Shape 2.1 (geometry), Hunyuan Paint 2.1 (PBR textures), + TRELLIS 2 (geometry+PBR), YVO3D (premium texturing up to 8K). +- After neural import, ALWAYS run: cleanup β†’ normalize β†’ decimate β†’ UV unwrap β†’ PBR material setup. + Use the import_neural_mesh.py RAG script for the full pipeline. +- Neural meshes MUST be retopologized before rigging β€” use Quadriflow (target 5-10k faces). + SCENE GROUNDING β€” CRITICAL: - ALWAYS add a floor plane unless the scene is explicitly set in space/void. Objects floating in blank space look unprofessional. Use bpy.ops.mesh.primitive_plane_add(). @@ -309,49 +334,3 @@ Suggest a recovery action as JSON: }}`), ]) -// ============================================================================ -// Few-Shot Examples -// ============================================================================ - -export const BLENDER_FEW_SHOT_EXAMPLES = ` -Example 1 - Create a blue sphere: -\`\`\`python -import bpy - -# Create sphere -bpy.ops.object.select_all(action='DESELECT') -bpy.ops.mesh.primitive_uv_sphere_add(radius=1.0, location=(0, 0, 1)) -sphere = bpy.context.active_object -sphere.name = 'Blue_Sphere' - -# Apply blue material -mat = bpy.data.materials.get('ModelForge_Blue') -if mat is None: - mat = bpy.data.materials.new('ModelForge_Blue') - bsdf = mat.node_tree.nodes.get('Principled BSDF') - bsdf.inputs['Base Color'].default_value = (0.1, 0.3, 0.9, 1.0) - -if not sphere.data.materials: - sphere.data.materials.append(mat) -else: - sphere.data.materials[0] = mat -\`\`\` - -Example 2 - Add lighting and camera: -\`\`\`python -import bpy - -# Add area light -if not any(obj.type == 'LIGHT' for obj in bpy.context.scene.objects): - bpy.ops.object.light_add(type='AREA', location=(5, -5, 7)) - light = bpy.context.active_object - light.data.energy = 1000 - -# Add camera -if not bpy.context.scene.camera: - bpy.ops.object.camera_add(location=(8, -8, 5)) - cam = bpy.context.active_object - cam.rotation_euler = (1.1, 0, 0.8) - bpy.context.scene.camera = cam -\`\`\` -` diff --git a/lib/gemini.ts b/lib/gemini.ts index 3737e45..0b0f24a 100644 --- a/lib/gemini.ts +++ b/lib/gemini.ts @@ -1,7 +1,7 @@ import { buildSystemPrompt } from "@/lib/orchestration/prompts" const GEMINI_API_ENDPOINT = "https://generativelanguage.googleapis.com/v1" -const DEFAULT_MODEL = "gemini-2.5-pro" +const DEFAULT_MODEL = "gemini-3.1-pro-preview" export interface GeminiMessage { role: "user" | "assistant" diff --git a/lib/mcp/screenshot.ts b/lib/mcp/screenshot.ts index 74b000e..3002506 100644 --- a/lib/mcp/screenshot.ts +++ b/lib/mcp/screenshot.ts @@ -24,11 +24,7 @@ export async function getViewportScreenshot(options: { try { const response = await client.execute({ type: "get_viewport_screenshot", - params: { - width: options.width ?? 1920, - height: options.height ?? 1080, - format: options.format ?? "png", - }, + params: {}, }) if (response.status === "error") { diff --git a/lib/neural/base-client.ts b/lib/neural/base-client.ts new file mode 100644 index 0000000..fc7c087 --- /dev/null +++ b/lib/neural/base-client.ts @@ -0,0 +1,137 @@ +/** + * Neural 3D Generation β€” Abstract Base Client + * + * Every neural provider (Hunyuan Shape, Hunyuan Paint, TRELLIS, YVO3D, etc.) + * extends this class. It provides shared utilities for downloading models, + * preparing images, and polling async jobs. + */ + +import fs from "fs/promises" +import path from "path" +import type { + GenerationRequest, + GenerationResult, + Neural3DProviderMeta, + ProviderSlug, +} from "./types" + +/** Default output directory for downloaded neural models */ +const DEFAULT_OUTPUT_DIR = + process.env.NEURAL_3D_OUTPUT_DIR ?? path.join(process.cwd(), "tmp", "neural-output") + +export abstract class Neural3DClient { + /** Human-readable provider name (e.g. "Hunyuan Shape 2.1") */ + abstract readonly name: string + + /** Unique provider slug used in the registry / config */ + abstract readonly slug: ProviderSlug + + /** Static metadata about this provider's capabilities */ + abstract readonly meta: Neural3DProviderMeta + + // ------------------------------------------------------------------------- + // Abstract methods β€” every provider MUST implement these + // ------------------------------------------------------------------------- + + /** Run a generation request and return a result. */ + abstract generate(request: GenerationRequest): Promise + + /** Check whether the backing server / API is reachable. */ + abstract healthCheck(): Promise + + // ------------------------------------------------------------------------- + // Shared utilities + // ------------------------------------------------------------------------- + + /** + * Download a remote model file (GLB/OBJ) to a local directory. + * + * @returns The absolute local file path of the saved model. + */ + protected async downloadModel( + url: string, + filename?: string, + outDir: string = DEFAULT_OUTPUT_DIR, + ): Promise { + await fs.mkdir(outDir, { recursive: true }) + + const resolvedFilename = + filename ?? `${this.slug}-${Date.now()}.glb` + const destPath = path.join(outDir, resolvedFilename) + + const response = await fetch(url) + if (!response.ok) { + throw new Error( + `Failed to download model from ${url}: ${response.status} ${response.statusText}`, + ) + } + + const buffer = Buffer.from(await response.arrayBuffer()) + await fs.writeFile(destPath, buffer) + + return destPath + } + + /** + * Save raw binary data (e.g. from an API response body) to a local file. + */ + protected async saveModelFromBuffer( + data: Buffer, + filename?: string, + outDir: string = DEFAULT_OUTPUT_DIR, + ): Promise { + await fs.mkdir(outDir, { recursive: true }) + + const resolvedFilename = + filename ?? `${this.slug}-${Date.now()}.glb` + const destPath = path.join(outDir, resolvedFilename) + await fs.writeFile(destPath, data) + + return destPath + } + + /** + * Read a local image file and return it as a base64 data URI string. + */ + protected async imageToBase64(imagePath: string): Promise { + const buf = await fs.readFile(imagePath) + const ext = path.extname(imagePath).replace(".", "").toLowerCase() + const mime = + ext === "png" + ? "image/png" + : ext === "webp" + ? "image/webp" + : "image/jpeg" + return `data:${mime};base64,${buf.toString("base64")}` + } + + /** + * Generic async-job poll loop. + * + * Repeatedly calls `pollFn` at the given interval until it returns a + * result with status "completed" or "failed", or until `maxWaitMs` + * elapses. + */ + protected async pollUntilDone( + pollFn: () => Promise, + intervalMs: number = 3_000, + maxWaitMs: number = 300_000, + ): Promise { + const deadline = Date.now() + maxWaitMs + + while (Date.now() < deadline) { + const result = await pollFn() + if (result.status === "completed" || result.status === "failed") { + return result + } + await new Promise((r) => setTimeout(r, intervalMs)) + } + + return { + status: "failed", + provider: this.slug, + stage: "geometry", + error: `Timed out after ${maxWaitMs / 1000}s waiting for generation to complete.`, + } + } +} diff --git a/lib/neural/gradio-client.d.ts b/lib/neural/gradio-client.d.ts new file mode 100644 index 0000000..76223c1 --- /dev/null +++ b/lib/neural/gradio-client.d.ts @@ -0,0 +1,19 @@ +/** + * Type declarations for @gradio/client (optional dependency). + * + * @gradio/client is only used by TRELLIS 2 and Hunyuan Part providers. + * It is loaded via dynamic import so the dependency is optional. + */ +declare module "@gradio/client" { + export interface PredictResult { + data: unknown[] + } + + export interface GradioClient { + predict(endpoint: string, data: Record): Promise + } + + export const Client: { + connect(url: string): Promise + } +} diff --git a/lib/neural/hybrid-pipeline.ts b/lib/neural/hybrid-pipeline.ts new file mode 100644 index 0000000..51eed30 --- /dev/null +++ b/lib/neural/hybrid-pipeline.ts @@ -0,0 +1,383 @@ +/** + * Hybrid Pipeline β€” Full Neural-to-Production Orchestrator + * + * Chains neural generation with Blender post-processing to produce + * production-ready 3D assets: + * + * 1. Generate geometry (Hunyuan Shape / TRELLIS) + * 2. Texture with PBR (Hunyuan Paint / YVO3D) + * 3. Import into Blender (MCP execute_code) + * 4. Auto-retopology (Blender Quadriflow) + * 5. Clean UV unwrap (Blender Smart UV / Lightmap UV) + * 6. Segment into parts (Hunyuan Part β€” optional) + * 7. Auto-rig with Rigify (Blender β€” optional) + * 8. Procedural animation (Blender β€” optional) + * 9. Validate scene (Vision feedback loop) + * 10. Export to target format (Blender export pipeline) + */ + +import type { + GenerationRequest, + HybridPipelineOptions, + PipelineStage, + PipelineStageStatus, + ProviderSlug, +} from "./types" +import { createNeuralClient, selectBestProvider } from "./registry" + +export interface HybridPipelineResult { + success: boolean + stages: PipelineStageStatus[] + /** Final exported model path (if export succeeded) */ + outputPath?: string + totalDurationMs: number +} + +export type PipelineProgressCallback = (stage: PipelineStageStatus) => void + +/** + * Execute the full hybrid pipeline: Neural generation β†’ Blender refinement β†’ Export + * + * @param request - The generation input (prompt/image) + * @param options - Pipeline configuration (which providers, which stages) + * @param onProgress - Optional callback for real-time stage updates + * @param executeMcp - Callback to execute Blender commands via MCP + */ +export async function runHybridPipeline( + request: Omit, + options: HybridPipelineOptions = {}, + onProgress?: PipelineProgressCallback, + executeMcp?: (code: string) => Promise<{ success: boolean; result?: string; error?: string }>, +): Promise { + const startTime = Date.now() + const stages: PipelineStageStatus[] = [] + + const { + geometryProvider = "hunyuan-shape", + textureProvider = "hunyuan-paint", + enableSegmentation = false, + enableRigging = false, + enableAnimation = false, + exportFormats = ["glb"], + gracefulDegradation = true, + } = options + + // Helper to track stage status + const trackStage = ( + stage: PipelineStage, + status: PipelineStageStatus["status"], + provider?: ProviderSlug, + extra?: Partial, + ) => { + const entry: PipelineStageStatus = { + stage, + status, + provider, + ...extra, + } + // Update or add + const idx = stages.findIndex((s) => s.stage === stage) + if (idx >= 0) stages[idx] = entry + else stages.push(entry) + onProgress?.(entry) + } + + let currentMeshPath: string | undefined + + // ----------------------------------------------------------------------- + // Stage 1: Geometry Generation + // ----------------------------------------------------------------------- + try { + trackStage("geometry", "running", geometryProvider) + + const geoMeta = selectBestProvider("geometry", geometryProvider) + if (!geoMeta) throw new Error("No geometry provider available") + + const geoClient = await createNeuralClient(geoMeta.slug) + const geoResult = await geoClient.generate({ + ...request, + provider: geoMeta.slug, + mode: request.imageUrl ? "image_to_3d" : "text_to_3d", + }) + + if (geoResult.status === "completed" && geoResult.modelPath) { + currentMeshPath = geoResult.modelPath + trackStage("geometry", "completed", geoMeta.slug, { + durationMs: geoResult.generationTimeMs, + outputPath: geoResult.modelPath, + }) + } else { + throw new Error(geoResult.error ?? "Geometry generation failed") + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + trackStage("geometry", "failed", geometryProvider, { error: msg }) + if (!gracefulDegradation) { + return { success: false, stages, totalDurationMs: Date.now() - startTime } + } + } + + // ----------------------------------------------------------------------- + // Stage 2: Texturing + // ----------------------------------------------------------------------- + if (currentMeshPath) { + try { + trackStage("texturing", "running", textureProvider) + + const texMeta = selectBestProvider("texturing", textureProvider) + if (!texMeta) throw new Error("No texturing provider available") + + const texClient = await createNeuralClient(texMeta.slug) + const texResult = await texClient.generate({ + ...request, + meshUrl: currentMeshPath, + provider: texMeta.slug, + mode: "mesh_to_texture", + }) + + if (texResult.status === "completed" && texResult.modelPath) { + currentMeshPath = texResult.modelPath + trackStage("texturing", "completed", texMeta.slug, { + durationMs: texResult.generationTimeMs, + outputPath: texResult.modelPath, + }) + } else { + throw new Error(texResult.error ?? "Texturing failed") + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + trackStage("texturing", "failed", textureProvider, { error: msg }) + if (!gracefulDegradation) { + return { success: false, stages, totalDurationMs: Date.now() - startTime } + } + } + } + + // ----------------------------------------------------------------------- + // Stage 3: Import into Blender (via MCP) + // ----------------------------------------------------------------------- + if (currentMeshPath && executeMcp) { + const importCode = ` +import bpy + +# Clear default scene +bpy.ops.object.select_all(action='SELECT') +bpy.ops.object.delete(use_global=False) + +# Import the neural-generated GLB +bpy.ops.import_scene.gltf(filepath=r"${currentMeshPath.replace(/\\/g, "/")}") + +# Center and ground the imported object +imported = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH'] +if imported: + bpy.context.view_layer.objects.active = imported[0] + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN') + for obj in imported: + obj.location.z -= min(v.co.z for v in obj.data.vertices) * obj.scale.z + +print(f"Imported {len(imported)} mesh objects from neural generation") +`.trim() + + try { + const result = await executeMcp(importCode) + if (!result.success) { + trackStage("retopology", "skipped", undefined, { + error: `Blender import failed: ${result.error}`, + }) + } + } catch { + // Non-fatal β€” continue with remaining stages + } + } + + // ----------------------------------------------------------------------- + // Stage 4: Retopology (Blender Quadriflow) + // ----------------------------------------------------------------------- + if (currentMeshPath && executeMcp) { + try { + trackStage("retopology", "running") + + const retopoCode = ` +import bpy + +obj = bpy.context.active_object +if obj and obj.type == 'MESH': + # Voxel remesh for watertight mesh first + mod = obj.modifiers.new(name="Voxel Remesh", type='REMESH') + mod.mode = 'VOXEL' + mod.voxel_size = 0.02 + bpy.ops.object.modifier_apply(modifier=mod.name) + + # Quadriflow retopology + bpy.ops.object.quadriflow_remesh( + target_faces=5000, + use_paint_symmetry=False, + use_preserve_sharp=True, + use_preserve_boundary=True, + seed=42 + ) + print(f"Retopology complete: {len(obj.data.polygons)} faces") +else: + print("No active mesh object for retopology") +`.trim() + + const result = await executeMcp(retopoCode) + trackStage("retopology", result.success ? "completed" : "failed", undefined, { + error: result.success ? undefined : result.error, + }) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + trackStage("retopology", "failed", undefined, { error: msg }) + } + } else { + trackStage("retopology", "skipped") + } + + // ----------------------------------------------------------------------- + // Stage 5: Segmentation (Hunyuan Part β€” optional) + // ----------------------------------------------------------------------- + if (enableSegmentation && currentMeshPath) { + try { + trackStage("segmentation", "running", "hunyuan-part") + const partClient = await createNeuralClient("hunyuan-part") + const partResult = await partClient.generate({ + meshUrl: currentMeshPath, + provider: "hunyuan-part", + mode: "mesh_to_parts", + }) + + if (partResult.status === "completed" && partResult.modelPath) { + currentMeshPath = partResult.modelPath + trackStage("segmentation", "completed", "hunyuan-part", { + durationMs: partResult.generationTimeMs, + outputPath: partResult.modelPath, + }) + } else { + throw new Error(partResult.error ?? "Segmentation failed") + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + trackStage("segmentation", "failed", "hunyuan-part", { error: msg }) + } + } else { + trackStage("segmentation", "skipped") + } + + // ----------------------------------------------------------------------- + // Stage 6: Rigging (Blender Rigify β€” optional) + // ----------------------------------------------------------------------- + if (enableRigging && executeMcp) { + try { + trackStage("rigging", "running") + + const rigCode = ` +import bpy + +obj = bpy.context.active_object +if obj and obj.type == 'MESH': + # Add Rigify basic human metarig + bpy.ops.object.armature_human_metarig_add() + metarig = bpy.context.active_object + + # Scale metarig to fit object + obj_dims = obj.dimensions + meta_dims = metarig.dimensions + scale_factor = max(obj_dims) / max(meta_dims) if max(meta_dims) > 0 else 1 + metarig.scale = (scale_factor, scale_factor, scale_factor) + bpy.ops.object.transform_apply(scale=True) + + # Generate rig + bpy.ops.pose.rigify_generate() + rig = bpy.context.active_object + + # Parent mesh to rig with automatic weights + obj.select_set(True) + rig.select_set(True) + bpy.context.view_layer.objects.active = rig + bpy.ops.object.parent_set(type='ARMATURE_AUTO') + + print(f"Rigging complete: {len(rig.data.bones)} bones") +else: + print("No active mesh for rigging") +`.trim() + + const result = await executeMcp(rigCode) + trackStage("rigging", result.success ? "completed" : "failed", undefined, { + error: result.success ? undefined : result.error, + }) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + trackStage("rigging", "failed", undefined, { error: msg }) + } + } else { + trackStage("rigging", "skipped") + } + + // ----------------------------------------------------------------------- + // Stage 7: Animation (optional) + // ----------------------------------------------------------------------- + if (enableAnimation && executeMcp) { + trackStage("animation", "skipped", undefined, { + error: "Procedural animation requires per-asset configuration β€” use RAG scripts directly", + }) + } else { + trackStage("animation", "skipped") + } + + // ----------------------------------------------------------------------- + // Stage 8: Export + // ----------------------------------------------------------------------- + if (executeMcp) { + try { + trackStage("export", "running") + + const format = exportFormats[0] ?? "glb" + const exportPath = currentMeshPath?.replace(/\.[^.]+$/, `-final.${format}`) ?? + `neural-export-${Date.now()}.${format}` + + const exportCode = ` +import bpy + +bpy.ops.object.select_all(action='SELECT') +filepath = r"${exportPath.replace(/\\/g, "/")}" +${format === "glb" ? `bpy.ops.export_scene.gltf(filepath=filepath, export_format='GLB')` : ""} +${format === "fbx" ? `bpy.ops.export_scene.fbx(filepath=filepath)` : ""} +${format === "obj" ? `bpy.ops.wm.obj_export(filepath=filepath)` : ""} +${format === "usd" ? `bpy.ops.wm.usd_export(filepath=filepath)` : ""} +print(f"Exported to {filepath}") +`.trim() + + const result = await executeMcp(exportCode) + trackStage("export", result.success ? "completed" : "failed", undefined, { + outputPath: result.success ? exportPath : undefined, + error: result.success ? undefined : result.error, + }) + + if (result.success) { + return { + success: true, + stages, + outputPath: exportPath, + totalDurationMs: Date.now() - startTime, + } + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + trackStage("export", "failed", undefined, { error: msg }) + } + } else { + trackStage("export", "skipped", undefined, { + error: "No MCP executor available β€” cannot export from Blender", + }) + } + + const allCompleted = stages.every((s) => + s.status === "completed" || s.status === "skipped", + ) + + return { + success: allCompleted, + stages, + totalDurationMs: Date.now() - startTime, + } +} diff --git a/lib/neural/index.ts b/lib/neural/index.ts new file mode 100644 index 0000000..92055d4 --- /dev/null +++ b/lib/neural/index.ts @@ -0,0 +1,35 @@ +/** + * Neural 3D Generation β€” Public API + * + * Barrel exports for lib/neural/* + */ + +// Types +export type { + ProviderSlug, + PipelineStage, + GenerationMode, + Neural3DProviderMeta, + GenerationRequest, + GenerationResult, + HybridPipelineOptions, + PipelineStageStatus, +} from "./types" + +// Base class +export { Neural3DClient } from "./base-client" + +// Registry +export { + PROVIDERS, + getProvidersForStage, + selectBestProvider, + createNeuralClient, +} from "./registry" + +// Hybrid Pipeline +export { runHybridPipeline } from "./hybrid-pipeline" +export type { + HybridPipelineResult, + PipelineProgressCallback, +} from "./hybrid-pipeline" diff --git a/lib/neural/providers/hunyuan-paint.ts b/lib/neural/providers/hunyuan-paint.ts new file mode 100644 index 0000000..509b181 --- /dev/null +++ b/lib/neural/providers/hunyuan-paint.ts @@ -0,0 +1,131 @@ +/** + * Hunyuan3D Paint 2.1 β€” PBR Texture Generation Client + * + * Takes an untextured mesh + reference image and generates production-ready + * PBR textures (albedo, roughness, metallic, normal). + * + * Uses the same server as Hunyuan Shape (api_server.py handles both models). + * + * Env vars: + * - HUNYUAN_API_URL (same server as Shape β€” runs both models) + */ + +import { Neural3DClient } from "../base-client" +import type { + GenerationRequest, + GenerationResult, + Neural3DProviderMeta, + ProviderSlug, +} from "../types" +import { PROVIDERS } from "../registry" +import fs from "fs/promises" + +export class HunyuanPaintClient extends Neural3DClient { + readonly name = "Hunyuan3D Paint 2.1" + readonly slug: ProviderSlug = "hunyuan-paint" + readonly meta: Neural3DProviderMeta = PROVIDERS["hunyuan-paint"] + + private readonly baseUrl: string + + constructor() { + super() + this.baseUrl = process.env.HUNYUAN_API_URL ?? "http://localhost:8080" + } + + async healthCheck(): Promise { + try { + const res = await fetch(`${this.baseUrl}/health`, { + method: "GET", + signal: AbortSignal.timeout(5_000), + }) + return res.ok + } catch { + return false + } + } + + async generate(request: GenerationRequest): Promise { + const startTime = Date.now() + + try { + if (!request.meshUrl) { + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: "Hunyuan Paint requires a meshUrl (path to untextured mesh).", + } + } + + // Read the mesh file + let meshBase64: string + if (request.meshUrl.startsWith("http")) { + const meshRes = await fetch(request.meshUrl) + const buf = Buffer.from(await meshRes.arrayBuffer()) + meshBase64 = buf.toString("base64") + } else { + const buf = await fs.readFile(request.meshUrl) + meshBase64 = buf.toString("base64") + } + + // Reference image for texture guidance + let imageBase64: string | undefined + if (request.imageUrl) { + if (request.imageUrl.startsWith("data:")) { + imageBase64 = request.imageUrl + } else if (request.imageUrl.startsWith("http")) { + const imgRes = await fetch(request.imageUrl) + const buf = Buffer.from(await imgRes.arrayBuffer()) + imageBase64 = `data:image/png;base64,${buf.toString("base64")}` + } else { + imageBase64 = await this.imageToBase64(request.imageUrl) + } + } + + const payload: Record = { + mesh: meshBase64, + image: imageBase64, + output_format: request.outputFormat ?? "glb", + } + + const response = await fetch(`${this.baseUrl}/texturize`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + const errText = await response.text() + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: `Hunyuan Paint API error ${response.status}: ${errText}`, + generationTimeMs: Date.now() - startTime, + } + } + + const modelBuffer = Buffer.from(await response.arrayBuffer()) + const modelPath = await this.saveModelFromBuffer( + modelBuffer, + `hunyuan-paint-${Date.now()}.glb`, + ) + + return { + status: "completed", + modelPath, + provider: this.slug, + stage: "texturing", + generationTimeMs: Date.now() - startTime, + } + } catch (err) { + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: `Hunyuan Paint generation failed: ${err instanceof Error ? err.message : String(err)}`, + generationTimeMs: Date.now() - startTime, + } + } + } +} diff --git a/lib/neural/providers/hunyuan-part.ts b/lib/neural/providers/hunyuan-part.ts new file mode 100644 index 0000000..b7abab2 --- /dev/null +++ b/lib/neural/providers/hunyuan-part.ts @@ -0,0 +1,118 @@ +/** + * Hunyuan3D Part β€” Mesh Segmentation Client + * + * Splits a 3D mesh into semantic sub-parts (gears, connectors, housings, etc.). + * Useful for mechanical objects, kitbashing, or preparing models for rigging. + * + * Can connect to: + * - Self-hosted Gradio app + * - HuggingFace Space (tencent/Hunyuan3D-Part) + * + * Env vars: + * - HUNYUAN_PART_URL (e.g. http://localhost:7861 or HF Space URL) + */ + +import { Neural3DClient } from "../base-client" +import type { + GenerationRequest, + GenerationResult, + Neural3DProviderMeta, + ProviderSlug, +} from "../types" +import { PROVIDERS } from "../registry" +import fs from "fs/promises" + +export class HunyuanPartClient extends Neural3DClient { + readonly name = "Hunyuan3D Part" + readonly slug: ProviderSlug = "hunyuan-part" + readonly meta: Neural3DProviderMeta = PROVIDERS["hunyuan-part"] + + private readonly baseUrl: string + + constructor() { + super() + this.baseUrl = + process.env.HUNYUAN_PART_URL ?? "https://tencent-hunyuan3d-part.hf.space" + } + + async healthCheck(): Promise { + try { + const res = await fetch(this.baseUrl, { + method: "GET", + signal: AbortSignal.timeout(10_000), + }) + return res.ok + } catch { + return false + } + } + + async generate(request: GenerationRequest): Promise { + const startTime = Date.now() + + try { + if (!request.meshUrl) { + return { + status: "failed", + provider: this.slug, + stage: "segmentation", + error: "Hunyuan Part requires a meshUrl to segment.", + } + } + + // Use the @gradio/client to interact with the Gradio app + // Dynamic import so we don't force the dependency on all users + const { Client } = await import("@gradio/client") + const client = await Client.connect(this.baseUrl) + + // Read the input mesh file + let meshBlob: Blob + if (request.meshUrl.startsWith("http")) { + const meshRes = await fetch(request.meshUrl) + meshBlob = await meshRes.blob() + } else { + const buf = await fs.readFile(request.meshUrl) + meshBlob = new Blob([new Uint8Array(buf)]) + } + + // Submit to the Gradio predict endpoint + const result = await client.predict("/predict", { + input_model: meshBlob, + }) + + // The result contains segmented model file(s) + const data = result.data as Array<{ url?: string; path?: string }> + if (data.length > 0 && (data[0].url || data[0].path)) { + const outputUrl = data[0].url ?? data[0].path! + const modelPath = await this.downloadModel( + outputUrl, + `hunyuan-part-${Date.now()}.glb`, + ) + + return { + status: "completed", + modelPath, + provider: this.slug, + stage: "segmentation", + generationTimeMs: Date.now() - startTime, + } + } + + return { + status: "failed", + provider: this.slug, + stage: "segmentation", + error: "Hunyuan Part returned no output data.", + generationTimeMs: Date.now() - startTime, + } + } catch (err) { + return { + status: "failed", + provider: this.slug, + stage: "segmentation", + error: `Hunyuan Part segmentation failed: ${err instanceof Error ? err.message : String(err)}`, + generationTimeMs: Date.now() - startTime, + } + } + } +} diff --git a/lib/neural/providers/hunyuan-shape.ts b/lib/neural/providers/hunyuan-shape.ts new file mode 100644 index 0000000..e382cdb --- /dev/null +++ b/lib/neural/providers/hunyuan-shape.ts @@ -0,0 +1,134 @@ +/** + * Hunyuan3D Shape 2.1 β€” Geometry Generation Client + * + * Connects to the self-hosted `api_server.py` from the official + * Hunyuan3D-2.1 repository (https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1). + * + * Capabilities: + * - image_to_3d: Reference image β†’ untextured mesh + * - text_to_3d: Text prompt β†’ untextured mesh (via built-in caption model) + * + * Env vars: + * - HUNYUAN_API_URL (e.g. http://localhost:8080 or Azure endpoint URL) + */ + +import { Neural3DClient } from "../base-client" +import type { + GenerationRequest, + GenerationResult, + Neural3DProviderMeta, + ProviderSlug, +} from "../types" +import { PROVIDERS } from "../registry" + +export class HunyuanShapeClient extends Neural3DClient { + readonly name = "Hunyuan3D Shape 2.1" + readonly slug: ProviderSlug = "hunyuan-shape" + readonly meta: Neural3DProviderMeta = PROVIDERS["hunyuan-shape"] + + private readonly baseUrl: string + + constructor() { + super() + this.baseUrl = process.env.HUNYUAN_API_URL ?? "http://localhost:8080" + } + + // ------------------------------------------------------------------------- + // Health check β€” hit the API server's root or /health endpoint + // ------------------------------------------------------------------------- + + async healthCheck(): Promise { + try { + const res = await fetch(`${this.baseUrl}/health`, { + method: "GET", + signal: AbortSignal.timeout(5_000), + }) + return res.ok + } catch { + // If /health doesn't exist, try a simple TCP probe to root + try { + const res = await fetch(this.baseUrl, { + method: "GET", + signal: AbortSignal.timeout(5_000), + }) + return res.ok + } catch { + return false + } + } + } + + // ------------------------------------------------------------------------- + // Generate β€” call the Hunyuan3D api_server.py /generate endpoint + // ------------------------------------------------------------------------- + + async generate(request: GenerationRequest): Promise { + const startTime = Date.now() + + try { + // Build the request payload for Hunyuan3D api_server + const payload: Record = { + output_format: request.outputFormat ?? "glb", + } + + if (request.imageUrl) { + // If it's already base64, use directly; otherwise read from path + if (request.imageUrl.startsWith("data:")) { + payload.image = request.imageUrl + } else if (request.imageUrl.startsWith("http")) { + // Download and convert to base64 + const imgRes = await fetch(request.imageUrl) + const buf = Buffer.from(await imgRes.arrayBuffer()) + payload.image = `data:image/png;base64,${buf.toString("base64")}` + } else { + // Local file path + payload.image = await this.imageToBase64(request.imageUrl) + } + } + + if (request.prompt) { + payload.text = request.prompt + } + + const response = await fetch(`${this.baseUrl}/generate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + const errText = await response.text() + return { + status: "failed", + provider: this.slug, + stage: "geometry", + error: `Hunyuan Shape API error ${response.status}: ${errText}`, + generationTimeMs: Date.now() - startTime, + } + } + + // The api_server returns the GLB binary directly in the response body + const modelBuffer = Buffer.from(await response.arrayBuffer()) + const modelPath = await this.saveModelFromBuffer( + modelBuffer, + `hunyuan-shape-${Date.now()}.glb`, + ) + + return { + status: "completed", + modelPath, + provider: this.slug, + stage: "geometry", + generationTimeMs: Date.now() - startTime, + } + } catch (err) { + return { + status: "failed", + provider: this.slug, + stage: "geometry", + error: `Hunyuan Shape generation failed: ${err instanceof Error ? err.message : String(err)}`, + generationTimeMs: Date.now() - startTime, + } + } + } +} diff --git a/lib/neural/providers/trellis.ts b/lib/neural/providers/trellis.ts new file mode 100644 index 0000000..1badccd --- /dev/null +++ b/lib/neural/providers/trellis.ts @@ -0,0 +1,124 @@ +/** + * TRELLIS 2 (Microsoft) β€” Geometry + PBR Texture Client + * + * 4B-parameter open-source model (MIT license). + * Generates high-resolution 3D assets with PBR textures from images. + * Supports resolutions up to 1536Β³. + * + * Connects to a self-hosted Gradio app or custom FastAPI wrapper. + * + * Env vars: + * - TRELLIS_API_URL (e.g. http://localhost:7860 or Azure endpoint) + */ + +import { Neural3DClient } from "../base-client" +import type { + GenerationRequest, + GenerationResult, + Neural3DProviderMeta, + ProviderSlug, +} from "../types" +import { PROVIDERS } from "../registry" +import fs from "fs/promises" + +export class TrellisClient extends Neural3DClient { + readonly name = "TRELLIS 2 (Microsoft)" + readonly slug: ProviderSlug = "trellis" + readonly meta: Neural3DProviderMeta = PROVIDERS["trellis"] + + private readonly baseUrl: string + + constructor() { + super() + this.baseUrl = process.env.TRELLIS_API_URL ?? "http://localhost:7860" + } + + async healthCheck(): Promise { + try { + const res = await fetch(this.baseUrl, { + method: "GET", + signal: AbortSignal.timeout(10_000), + }) + return res.ok + } catch { + return false + } + } + + async generate(request: GenerationRequest): Promise { + const startTime = Date.now() + + try { + if (!request.imageUrl) { + return { + status: "failed", + provider: this.slug, + stage: "geometry", + error: "TRELLIS 2 requires an imageUrl for image-to-3D generation.", + } + } + + // Use @gradio/client to submit to the TRELLIS 2 Gradio app + const { Client } = await import("@gradio/client") + const client = await Client.connect(this.baseUrl) + + // Prepare image input + let imageBlob: Blob + if (request.imageUrl.startsWith("data:")) { + // Strip data URI prefix and decode + const base64Data = request.imageUrl.split(",")[1] + const buf = Buffer.from(base64Data, "base64") + imageBlob = new Blob([new Uint8Array(buf)], { type: "image/png" }) + } else if (request.imageUrl.startsWith("http")) { + const imgRes = await fetch(request.imageUrl) + imageBlob = await imgRes.blob() + } else { + const buf = await fs.readFile(request.imageUrl) + imageBlob = new Blob([new Uint8Array(buf)], { type: "image/png" }) + } + + // Submit generation β€” TRELLIS Gradio typically has /image_to_3d endpoint + const result = await client.predict("/image_to_3d", { + image: imageBlob, + // Resolution defaults differ by target: + // 512Β³ β†’ ~3s on H100 + // 1024Β³ β†’ ~15s + // 1536Β³ β†’ ~60s + }) + + // Parse the result β€” Gradio returns file references + const data = result.data as Array<{ url?: string; path?: string }> + if (data.length > 0 && (data[0].url || data[0].path)) { + const outputUrl = data[0].url ?? data[0].path! + const modelPath = await this.downloadModel( + outputUrl, + `trellis-${Date.now()}.glb`, + ) + + return { + status: "completed", + modelPath, + provider: this.slug, + stage: "geometry", + generationTimeMs: Date.now() - startTime, + } + } + + return { + status: "failed", + provider: this.slug, + stage: "geometry", + error: "TRELLIS 2 returned no output.", + generationTimeMs: Date.now() - startTime, + } + } catch (err) { + return { + status: "failed", + provider: this.slug, + stage: "geometry", + error: `TRELLIS 2 generation failed: ${err instanceof Error ? err.message : String(err)}`, + generationTimeMs: Date.now() - startTime, + } + } + } +} diff --git a/lib/neural/providers/yvo3d.ts b/lib/neural/providers/yvo3d.ts new file mode 100644 index 0000000..4c5dc79 --- /dev/null +++ b/lib/neural/providers/yvo3d.ts @@ -0,0 +1,227 @@ +/** + * YVO3D β€” Premium Texture Generation Client (Third-Party API) + * + * YVO3D specializes in ultra-high-quality texture generation (up to ULTIMA 8K). + * Supports: + * - image_to_3d: Image β†’ geometry-only model + * - mesh_to_texture: Existing mesh β†’ retextured with PBR materials + * + * REST API docs: https://api.yvo3d.com/docs/generation + * + * Env vars: + * - YVO3D_API_KEY (Bearer token for the YVO3D API) + */ + +import { Neural3DClient } from "../base-client" +import type { + GenerationRequest, + GenerationResult, + Neural3DProviderMeta, + ProviderSlug, +} from "../types" +import { PROVIDERS } from "../registry" + +const YVO3D_BASE = "https://api.yvo3d.com/v1" + +export class Yvo3dClient extends Neural3DClient { + readonly name = "YVO3D" + readonly slug: ProviderSlug = "yvo3d" + readonly meta: Neural3DProviderMeta = PROVIDERS["yvo3d"] + + private readonly apiKey: string + + constructor() { + super() + this.apiKey = process.env.YVO3D_API_KEY ?? "" + } + + private get headers() { + return { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + } + } + + async healthCheck(): Promise { + if (!this.apiKey) return false + try { + const res = await fetch(`${YVO3D_BASE}/status`, { + method: "GET", + headers: this.headers, + signal: AbortSignal.timeout(5_000), + }) + return res.ok + } catch { + return false + } + } + + async generate(request: GenerationRequest): Promise { + const startTime = Date.now() + + if (!this.apiKey) { + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: "YVO3D_API_KEY is not configured.", + } + } + + try { + let payload: Record + + if (request.mode === "mesh_to_texture" && request.meshUrl) { + // --------------------------------------------------------------- + // Model-to-Model: Retexture an existing mesh with PBR + // --------------------------------------------------------------- + payload = { + type: "model_to_model", + inputModelUrl: request.meshUrl, + textureResolution: request.textureResolution ?? "2K", + realismLevel: 0.9, + creativity: 3, + insideOut: "OFF", + unlit: "OFF", + generateUVs: "OFF", + hyperMode: request.turbo ? "ON" : "OFF", + textureSharpness: 0.7, + textureAdherence: "OFF", + roughness: 1.3, + } + + // If a reference image is provided, enable retexturing + if (request.imageUrl) { + // YVO3D requires uploading the image first via /v1/upload, + // but for now we pass the imageId directly if available + payload.retexture = "ON" + } + } else if (request.imageUrl) { + // --------------------------------------------------------------- + // Image-to-Texture-Model: Full generation from image + // --------------------------------------------------------------- + payload = { + type: "image_to_texture_model", + modelGenerator: request.turbo ? "FAST_CREATIVE" : "ULTIMA_PRIME", + textureResolution: request.textureResolution ?? "2K", + realismLevel: 0.9, + creativity: 3, + insideOut: "OFF", + unlit: "OFF", + generateUVs: "OFF", + hyperMode: request.turbo ? "ON" : "OFF", + textureSharpness: 0.7, + textureAdherence: "OFF", + roughness: 1.3, + } + } else { + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: "YVO3D requires either imageUrl or meshUrl.", + } + } + + // Submit generation job + const genRes = await fetch(`${YVO3D_BASE}/generate`, { + method: "POST", + headers: this.headers, + body: JSON.stringify(payload), + }) + + if (!genRes.ok) { + const errText = await genRes.text() + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: `YVO3D API error ${genRes.status}: ${errText}`, + generationTimeMs: Date.now() - startTime, + } + } + + const genData = (await genRes.json()) as { data?: { id?: string } } + const jobId = genData?.data?.id + if (!jobId) { + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: "YVO3D returned no job ID.", + generationTimeMs: Date.now() - startTime, + } + } + + // Poll for completion + return await this.pollUntilDone( + async (): Promise => { + const statusRes = await fetch(`${YVO3D_BASE}/status/${jobId}`, { + headers: this.headers, + }) + + if (!statusRes.ok) { + return { + status: "processing", + provider: this.slug, + stage: "texturing", + } + } + + const statusData = (await statusRes.json()) as { + data?: { + status?: string + progress?: number + result?: { model_url?: string } + } + } + + const jobStatus = statusData?.data?.status + + if (jobStatus === "completed" && statusData?.data?.result?.model_url) { + const modelPath = await this.downloadModel( + statusData.data.result.model_url, + `yvo3d-${Date.now()}.glb`, + ) + + return { + status: "completed", + modelPath, + provider: this.slug, + stage: "texturing", + progress: 100, + generationTimeMs: Date.now() - startTime, + } + } + + if (jobStatus === "failed") { + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: "YVO3D job failed.", + generationTimeMs: Date.now() - startTime, + } + } + + return { + status: "processing", + provider: this.slug, + stage: "texturing", + progress: statusData?.data?.progress, + } + }, + 5_000, // poll every 5s + 180_000, // max 3 minutes + ) + } catch (err) { + return { + status: "failed", + provider: this.slug, + stage: "texturing", + error: `YVO3D generation failed: ${err instanceof Error ? err.message : String(err)}`, + generationTimeMs: Date.now() - startTime, + } + } + } +} diff --git a/lib/neural/registry.ts b/lib/neural/registry.ts new file mode 100644 index 0000000..d55626f --- /dev/null +++ b/lib/neural/registry.ts @@ -0,0 +1,142 @@ +/** + * Neural 3D Generation β€” Provider Registry + * + * Central catalogue of all available neural providers, with helper functions + * to look up providers by pipeline stage, select the best one, and + * instantiate client instances via a factory. + */ + +import type { Neural3DProviderMeta, PipelineStage, ProviderSlug } from "./types" +import type { Neural3DClient } from "./base-client" + +// --------------------------------------------------------------------------- +// Provider metadata catalogue +// --------------------------------------------------------------------------- + +export const PROVIDERS: Record = { + "hunyuan-shape": { + slug: "hunyuan-shape", + name: "Hunyuan3D Shape 2.1", + stages: ["geometry"], + modes: ["image_to_3d", "text_to_3d"], + selfHosted: true, + outputFormats: ["glb", "obj"], + estimatedTime: { min: 10, max: 60 }, + vramGb: 10, + }, + "hunyuan-paint": { + slug: "hunyuan-paint", + name: "Hunyuan3D Paint 2.1", + stages: ["texturing"], + modes: ["mesh_to_texture"], + selfHosted: true, + outputFormats: ["glb"], + estimatedTime: { min: 15, max: 90 }, + vramGb: 21, + }, + "hunyuan-part": { + slug: "hunyuan-part", + name: "Hunyuan3D Part", + stages: ["segmentation"], + modes: ["mesh_to_parts"], + selfHosted: true, + outputFormats: ["glb", "obj"], + estimatedTime: { min: 5, max: 30 }, + }, + trellis: { + slug: "trellis", + name: "TRELLIS 2 (Microsoft)", + stages: ["geometry", "texturing"], + modes: ["image_to_3d"], + selfHosted: true, + outputFormats: ["glb"], + estimatedTime: { min: 3, max: 60 }, + vramGb: 24, + }, + yvo3d: { + slug: "yvo3d", + name: "YVO3D", + stages: ["texturing"], + modes: ["mesh_to_texture", "image_to_3d"], + selfHosted: false, + outputFormats: ["glb", "obj"], + estimatedTime: { min: 10, max: 120 }, + }, +} + +// --------------------------------------------------------------------------- +// Lookup helpers +// --------------------------------------------------------------------------- + +/** Return all providers that can handle the given pipeline stage. */ +export function getProvidersForStage(stage: PipelineStage): Neural3DProviderMeta[] { + return Object.values(PROVIDERS).filter((p) => p.stages.includes(stage)) +} + +/** + * Pick the best provider for a given pipeline stage. + * + * Strategy: + * 1. Prefer self-hosted providers (lower cost, full control). + * 2. Among self-hosted, pick the one with lower VRAM (more accessible). + * 3. Fall back to third-party APIs if no self-hosted option exists. + * 4. If `preferredSlug` is provided and valid for the stage, use it. + */ +export function selectBestProvider( + stage: PipelineStage, + preferredSlug?: ProviderSlug, +): Neural3DProviderMeta | null { + const candidates = getProvidersForStage(stage) + if (candidates.length === 0) return null + + // If the caller has a preference and it supports this stage, use it. + if (preferredSlug) { + const preferred = candidates.find((p) => p.slug === preferredSlug) + if (preferred) return preferred + } + + // Sort: self-hosted first, then by lower VRAM + const sorted = [...candidates].sort((a, b) => { + if (a.selfHosted !== b.selfHosted) return a.selfHosted ? -1 : 1 + return (a.vramGb ?? 999) - (b.vramGb ?? 999) + }) + + return sorted[0] +} + +// --------------------------------------------------------------------------- +// Client factory +// --------------------------------------------------------------------------- + +/** + * Create a concrete Neural3DClient for the given provider slug. + * + * Uses dynamic imports to avoid loading every provider's dependencies + * upfront (they may have heavy optional packages like @gradio/client). + */ +export async function createNeuralClient(slug: ProviderSlug): Promise { + switch (slug) { + case "hunyuan-shape": { + const { HunyuanShapeClient } = await import("./providers/hunyuan-shape") + return new HunyuanShapeClient() + } + case "hunyuan-paint": { + const { HunyuanPaintClient } = await import("./providers/hunyuan-paint") + return new HunyuanPaintClient() + } + case "hunyuan-part": { + const { HunyuanPartClient } = await import("./providers/hunyuan-part") + return new HunyuanPartClient() + } + case "trellis": { + const { TrellisClient } = await import("./providers/trellis") + return new TrellisClient() + } + case "yvo3d": { + const { Yvo3dClient } = await import("./providers/yvo3d") + return new Yvo3dClient() + } + default: + throw new Error(`Unknown neural provider slug: ${slug}`) + } +} diff --git a/lib/neural/types.ts b/lib/neural/types.ts new file mode 100644 index 0000000..7d3a6c0 --- /dev/null +++ b/lib/neural/types.ts @@ -0,0 +1,130 @@ +/** + * Neural 3D Generation β€” Core Types + * + * Defines the provider abstraction, pipeline stages, and request/result + * interfaces used by all neural provider clients. + */ + +// --------------------------------------------------------------------------- +// Provider & Pipeline Enums +// --------------------------------------------------------------------------- + +/** Unique slug for each neural provider implementation */ +export type ProviderSlug = + | "hunyuan-shape" + | "hunyuan-paint" + | "hunyuan-part" + | "trellis" + | "yvo3d" + +/** Stages of the full 3D production pipeline */ +export type PipelineStage = + | "geometry" + | "texturing" + | "retopology" + | "segmentation" + | "rigging" + | "animation" + | "export" + +/** The mode of generation supported by a provider */ +export type GenerationMode = + | "text_to_3d" + | "image_to_3d" + | "mesh_to_texture" + | "mesh_to_parts" + +// --------------------------------------------------------------------------- +// Provider Metadata +// --------------------------------------------------------------------------- + +/** Static metadata describing a neural provider's capabilities */ +export interface Neural3DProviderMeta { + slug: ProviderSlug + name: string + /** Pipeline stages this provider can handle */ + stages: PipelineStage[] + /** Generation modes supported */ + modes: GenerationMode[] + /** Whether we self-host or use a third-party API */ + selfHosted: boolean + /** Supported output mesh formats */ + outputFormats: string[] + /** Estimated generation time range in seconds */ + estimatedTime: { min: number; max: number } + /** Approximate VRAM requirement in GB (self-hosted only) */ + vramGb?: number +} + +// --------------------------------------------------------------------------- +// Generation Request / Result +// --------------------------------------------------------------------------- + +export interface GenerationRequest { + /** Text prompt (for text_to_3d mode) */ + prompt?: string + /** Reference image β€” base64-encoded data URI or public URL */ + imageUrl?: string + /** Existing mesh URL or local path (for mesh_to_texture / mesh_to_parts) */ + meshUrl?: string + /** Provider to use */ + provider: ProviderSlug + /** Generation mode */ + mode: GenerationMode + /** Voxel/mesh resolution (TRELLIS: 512/1024/1536) */ + resolution?: number + /** Texture resolution (YVO3D: "1K"|"2K"|"FAST4K"|"REAL4K"|"ULTIMA8K") */ + textureResolution?: string + /** Output format preference */ + outputFormat?: "glb" | "obj" + /** Enable turbo / fast mode where supported */ + turbo?: boolean +} + +export interface GenerationResult { + status: "pending" | "processing" | "completed" | "failed" + /** Progress 0-100 (if the provider supports incremental updates) */ + progress?: number + /** Local file path to the downloaded output model */ + modelPath?: string + /** Which provider produced this result */ + provider: ProviderSlug + /** Which pipeline stage was executed */ + stage: PipelineStage + /** Wall-clock generation time in milliseconds */ + generationTimeMs?: number + /** Error message if status === "failed" */ + error?: string +} + +// --------------------------------------------------------------------------- +// Hybrid Pipeline +// --------------------------------------------------------------------------- + +/** Configuration for the full hybrid pipeline run */ +export interface HybridPipelineOptions { + /** Provider for geometry stage (default: "hunyuan-shape") */ + geometryProvider?: ProviderSlug + /** Provider for texturing stage (default: "hunyuan-paint") */ + textureProvider?: ProviderSlug + /** Whether to run segmentation via Hunyuan Part */ + enableSegmentation?: boolean + /** Whether to auto-rig via Blender Rigify */ + enableRigging?: boolean + /** Whether to add procedural animation */ + enableAnimation?: boolean + /** Export format(s) */ + exportFormats?: ("glb" | "fbx" | "obj" | "usd")[] + /** Skip stages that fail instead of aborting the entire pipeline */ + gracefulDegradation?: boolean +} + +/** Status of one stage in the hybrid pipeline */ +export interface PipelineStageStatus { + stage: PipelineStage + status: "pending" | "running" | "completed" | "skipped" | "failed" + provider?: ProviderSlug + durationMs?: number + error?: string + outputPath?: string +} diff --git a/lib/orchestration/executor.ts b/lib/orchestration/executor.ts index 52acee8..a202fcb 100644 --- a/lib/orchestration/executor.ts +++ b/lib/orchestration/executor.ts @@ -7,7 +7,9 @@ import { formatContextFromSources } from "@/lib/ai/rag" import { createGeminiModel } from "@/lib/ai" import { type LlmProviderSpec } from "@/lib/llm" import { createMcpClient, getViewportScreenshot, type McpCommand } from "@/lib/mcp" +import { suggestImprovements } from "@/lib/ai/vision" import { ExecutionLogEntry, ExecutionPlan, PlanAnalysis, AgentStreamEvent } from "./types" +import type { StrategyDecision } from "./strategy-types" export interface ExecutionResult { success: boolean @@ -46,6 +48,8 @@ export interface ExecutionOptions { maxVisualIterations?: number /** Callback to stream real-time agent events to the client */ onStreamEvent?: (event: AgentStreamEvent) => void + /** Strategy decision from the router */ + strategyDecision?: StrategyDecision } export class PlanExecutor { @@ -111,6 +115,9 @@ export class PlanExecutor { const allowSketchfab = options.allowSketchfab const allowPolyHaven = options.allowPolyHaven + // Track scene state from get_scene_info/get_all_object_info for code gen context + let sceneObjectContext = "" + for (let i = 0; i < plan.steps.length; i++) { const result = await agent.executeStep(i, async (step) => { // Tool restriction checks @@ -167,7 +174,7 @@ export class PlanExecutor { try { generatedCode = await generateCode({ request: description, - context: `This is one step in a larger plan for: "${userRequest}". Generate code for ONLY the described task, not the entire plan.${ragContext ? `\n\n## Reference Blender Python Scripts\n${ragContext}` : ""}`, + context: `This is one step in a larger plan for: "${userRequest}". Generate code for ONLY the described task, not the entire plan.${sceneObjectContext ? `\n\n## Current Scene Objects\nThese objects already exist in the scene. Reference them by name, do NOT recreate them:\n${sceneObjectContext}` : ""}${ragContext ? `\n\n## Reference Blender Python Scripts\n${ragContext}` : ""}`, applyMaterials: true, namingPrefix: "ModelForge_", constraints: step.expected_outcome, @@ -198,12 +205,36 @@ export class PlanExecutor { const toolResult = await client.execute(command) + // Capture scene state from inspection steps for code gen context + if ((step.action === "get_scene_info" || step.action === "get_all_object_info") && toolResult) { + try { + const payload = typeof toolResult === "object" ? toolResult as Record : {} + const result = typeof payload.result === "object" && payload.result ? payload.result as Record : payload + const objects = Array.isArray(result.objects) ? result.objects : [] + const compactObjects = objects.slice(0, 30) + .filter((o): o is Record => !!o && typeof o === "object") + .map((o) => ({ + name: o.name, + type: o.type, + location: Array.isArray(o.location) ? o.location.slice(0, 3).map((v) => typeof v === "number" ? Math.round(v * 100) / 100 : 0) : undefined, + dimensions: Array.isArray(o.dimensions) ? o.dimensions.slice(0, 3).map((v) => typeof v === "number" ? Math.round(v * 100) / 100 : 0) : undefined, + })) + if (compactObjects.length > 0) { + sceneObjectContext = JSON.stringify(compactObjects, null, 2) + } + } catch { + // Non-fatal β€” scene context is a nice-to-have + } + } + // Log specific tool execution logs.push({ timestamp: new Date().toISOString(), tool: step.action, parameters: step.parameters ?? {}, result: toolResult, + logType: "execute", + detail: `Step result: ${step.action}`, }) return toolResult @@ -229,6 +260,165 @@ export class PlanExecutor { // Non-fatal β€” viewport shading is cosmetic } + // 4b. Visual Feedback Loop β€” capture viewport, analyze, and correct + const maxVisualIter = options.maxVisualIterations ?? 2 + if (options.enableVisualFeedback !== false) { + try { + for (let vIter = 0; vIter < maxVisualIter; vIter++) { + // Capture viewport screenshot via the existing MCP client + // Note: No params β€” the Blender MCP addon doesn't accept width/height/format + const screenshotResult = await client.execute({ + type: "get_viewport_screenshot", + params: {}, + }) + + // The MCP client returns McpResponse where .result contains the data. + // Blender MCP addons may nest the response differently, so try multiple paths. + const topLevel = screenshotResult as Record + const resultLevel = screenshotResult?.result as Record | undefined + + // Try: result.image (standard), then top-level image, then result.result.image (double-nested) + const imageBase64 = ( + resultLevel?.image ?? + topLevel?.image ?? + (resultLevel?.result as Record | undefined)?.image + ) as string | undefined + + if (!imageBase64) { + // Log the actual response shape for debugging + const debugKeys = { + topLevelKeys: Object.keys(topLevel || {}), + resultKeys: resultLevel ? Object.keys(resultLevel) : 'no result', + status: screenshotResult?.status, + message: screenshotResult?.message, + } + console.warn('[Executor] Screenshot data not found. Response shape:', JSON.stringify(debugKeys)) + logs.push({ + timestamp: new Date().toISOString(), + tool: "visual_feedback", + parameters: { iteration: vIter + 1, debugKeys }, + error: "No screenshot data received from Blender MCP", + logType: "vision", + detail: `Viewport screenshot capture failed β€” response keys: ${JSON.stringify(debugKeys)}`, + }) + break + } + + options.onStreamEvent?.({ + type: "agent:visual_analysis", + timestamp: new Date().toISOString(), + iteration: vIter + 1, + description: `Analyzing viewport (iteration ${vIter + 1}/${maxVisualIter})...`, + } as AgentStreamEvent) + + // Ask Gemini Vision to analyze the scene against the user request + const visionResult = await suggestImprovements(imageBase64, userRequest) + + logs.push({ + timestamp: new Date().toISOString(), + tool: "visual_analysis", + parameters: { iteration: vIter + 1, userRequest: userRequest.substring(0, 100) }, + result: { + currentState: visionResult.currentState, + missingElements: visionResult.missingElements, + improvementCount: visionResult.improvements.length, + highPriorityCount: visionResult.improvements.filter(i => i.priority === "high").length, + }, + logType: "vision", + detail: `Vision analysis: ${visionResult.improvements.length} improvements suggested (${visionResult.improvements.filter(i => i.priority === "high").length} high priority)`, + }) + + // Only correct if there are high-priority issues + const highPriority = visionResult.improvements.filter(i => i.priority === "high") + if (highPriority.length === 0) { + logs.push({ + timestamp: new Date().toISOString(), + tool: "visual_feedback", + parameters: { iteration: vIter + 1 }, + result: { action: "pass", reason: "No high-priority issues detected" }, + logType: "vision", + detail: "Visual check passed β€” no high-priority corrections needed", + }) + break // Scene looks good, proceed to audit + } + + // Generate correction code based on vision feedback + const correctionDescription = highPriority + .map(hp => `FIX: ${hp.action} (Reason: ${hp.rationale})`) + .join("\n") + + options.onStreamEvent?.({ + type: "agent:visual_correction", + timestamp: new Date().toISOString(), + iteration: vIter + 1, + description: `Correcting ${highPriority.length} visual issues...`, + issues: highPriority.map(hp => hp.action), + } as AgentStreamEvent) + + // Retrieve RAG context for correction + let correctionRagContext = "" + try { + const ragSources = await similaritySearch(correctionDescription, { + limit: 3, + source: "blender-scripts", + minSimilarity: 0.4, + }) + if (ragSources.length > 0) { + correctionRagContext = formatContextFromSources(ragSources) + } + } catch { /* Non-fatal */ } + + try { + const correctionCode = await generateCode({ + request: correctionDescription, + context: `VISUAL CORRECTION: The scene for "${userRequest}" was analyzed by vision and these issues were found. Generate Python code to FIX these specific issues ONLY. Do NOT recreate the entire scene β€” only adjust existing objects.\n\nCurrent scene state: ${visionResult.currentState}\nMissing elements: ${visionResult.missingElements.join(", ") || "none"}${correctionRagContext ? `\n\n## Reference Scripts\n${correctionRagContext}` : ""}`, + applyMaterials: false, + namingPrefix: "ModelForge_Fix_", + }) + + // Execute the correction + const correctionResult = await client.execute({ + type: "execute_code", + params: { code: correctionCode }, + }) + + logs.push({ + timestamp: new Date().toISOString(), + tool: "visual_correction", + parameters: { + iteration: vIter + 1, + issueCount: highPriority.length, + issues: highPriority.map(hp => hp.action), + }, + result: correctionResult, + logType: "vision", + detail: `Applied visual correction (${highPriority.length} issues) β€” iteration ${vIter + 1}`, + }) + } catch (correctionError) { + logs.push({ + timestamp: new Date().toISOString(), + tool: "visual_correction", + parameters: { iteration: vIter + 1 }, + error: correctionError instanceof Error ? correctionError.message : String(correctionError), + logType: "vision", + detail: "Visual correction code generation/execution failed (non-fatal)", + }) + break // Don't retry if correction itself fails + } + } + } catch (visionError) { + // Entire visual feedback loop is non-fatal + logs.push({ + timestamp: new Date().toISOString(), + tool: "visual_feedback", + parameters: {}, + error: visionError instanceof Error ? visionError.message : String(visionError), + logType: "vision", + detail: "Visual feedback loop failed (non-fatal) β€” proceeding to scene audit", + }) + } + } + // 5. Final Scene Audit try { const finalState = await client.execute({ type: "get_scene_info" }) diff --git a/lib/orchestration/planner.ts b/lib/orchestration/planner.ts index cdb426a..f54bc1d 100644 --- a/lib/orchestration/planner.ts +++ b/lib/orchestration/planner.ts @@ -4,6 +4,7 @@ import { type LlmProviderSpec } from "@/lib/llm" import { createConversationMemory, type ConversationMemory } from "@/lib/memory" import { filterRelevantTools } from "./tool-filter" import { ExecutionPlan, PlanAnalysis, PlanGenerationResult, PlanStep } from "./types" +import type { StrategyDecision } from "./strategy-types" interface PlanningOptions { sceneSummary?: string @@ -17,6 +18,8 @@ interface PlanningOptions { useMemory?: boolean /** Existing conversation memory instance */ memory?: ConversationMemory + /** Strategy decision from the router */ + strategyDecision?: StrategyDecision } export class BlenderPlanner { @@ -61,6 +64,11 @@ export class BlenderPlanner { if (memoryContext) { enhancedRequest = `${memoryContext}\n\n${enhancedRequest}` } + if (options.strategyDecision && options.strategyDecision.strategy !== "procedural") { + const sd = options.strategyDecision + const providers = sd.suggestedProviders?.join(", ") ?? "auto-select" + enhancedRequest += `\n\n[Strategy: ${sd.strategy.toUpperCase()}] This request should use ${sd.strategy} generation (${sd.reasoning}). Suggested providers: ${providers}. Include neural_generate steps in the plan where appropriate.` + } try { // 4. Generate Plan using LangChain Agent diff --git a/lib/orchestration/strategy-router.ts b/lib/orchestration/strategy-router.ts new file mode 100644 index 0000000..34b71db --- /dev/null +++ b/lib/orchestration/strategy-router.ts @@ -0,0 +1,270 @@ +import type { Strategy, StrategyDecision, StrategyOverride } from "./strategy-types" +import type { ProviderSlug } from "@/lib/neural/types" + +// --------------------------------------------------------------------------- +// Keyword-based classification patterns +// --------------------------------------------------------------------------- + +/** Signals that strongly indicate procedural (Blender Python) generation. */ +const PROCEDURAL_PATTERNS: RegExp[] = [ + // Architectural & structural + /\b(building|house|room|wall|floor|ceiling|door|window|staircase|stair|arch|column|bridge|tower|castle|temple|cabin|shed|warehouse)\b/i, + // Geometric primitives & parametric + /\b(cube|sphere|cylinder|cone|torus|plane|grid|prism|pyramid|helix|spiral|polygon|vertex|edge|mesh|parametric|boolean|extrude|bevel)\b/i, + // Furniture & interior + /\b(table|chair|desk|shelf|bookcase|lamp|sofa|bed|cabinet|drawer|mirror|frame|rug|carpet|curtain)\b/i, + // Mechanical & precision + /\b(gear|screw|bolt|pipe|duct|rail|track|frame|mount|bracket|hinge|axle|piston|wheel|engine)\b/i, + // Scene-level edits (these always use Blender Python) + /\b(move|rotate|scale|delete|duplicate|array|modify|adjust|position|layout|arrange|align)\b/i, + // Lighting & camera (always procedural) + /\b(light|lighting|camera|render|hdri|sun|spot|area|point light)\b/i, + // Materials & textures (always procedural via Blender nodes) + /\b(material|shader|texture|color|roughness|metallic|emission|glass|glossy|pbr|uv map)\b/i, +] + +/** Signals that strongly indicate neural 3D generation. */ +const NEURAL_PATTERNS: RegExp[] = [ + // Organic / characters + /\b(character|person|human|man|woman|child|face|portrait|head|body|figure|figurine|bust|statue)\b/i, + // Animals & creatures + /\b(animal|dog|cat|horse|bird|fish|dragon|creature|monster|dinosaur|insect|snake|wolf|bear|lion)\b/i, + // Nature & organic shapes + /\b(tree|plant|flower|leaf|branch|coral|mushroom|rock formation|terrain|mountain|cliff)\b/i, + // Sculpted / artistic + /\b(sculpture|sculpt|organic|freeform|natural shape|realistic model|photorealistic|lifelike)\b/i, + // Food & complex objects + /\b(food|fruit|vegetable|bread|cake|skull|shoe|clothing|garment|hat|bag|backpack)\b/i, +] + +/** Signals that suggest hybrid pipeline (neural + Blender post-processing). */ +const HYBRID_PATTERNS: RegExp[] = [ + // Character + rigging/animation + /\b(character|creature|person|human).{0,40}(rig|rigged|animate|animated|pose|posed|walk|run|dance)\b/i, + /\b(rig|animate|pose).{0,40}(character|creature|person|human|animal)\b/i, + // Realistic + game/production ready + /\b(realistic|organic|character|creature).{0,40}(game.?ready|production|export|fbx|gltf|usd)\b/i, + // Neural + specific Blender ops + /\b(generate|create|make).{0,40}(then|and).{0,40}(rig|texture|animate|retopolog)/i, +] + +// --------------------------------------------------------------------------- +// Keyword scoring +// --------------------------------------------------------------------------- + +interface KeywordScore { + procedural: number + neural: number + hybrid: number +} + +function scoreKeywords(request: string): KeywordScore { + const score: KeywordScore = { procedural: 0, neural: 0, hybrid: 0 } + + // Hybrid checked first β€” it's context-dependent combinations + for (const pattern of HYBRID_PATTERNS) { + if (pattern.test(request)) score.hybrid += 2 + } + + for (const pattern of PROCEDURAL_PATTERNS) { + if (pattern.test(request)) score.procedural += 1 + } + + for (const pattern of NEURAL_PATTERNS) { + if (pattern.test(request)) score.neural += 1 + } + + return score +} + +function decideFromKeywords(score: KeywordScore): StrategyDecision | null { + const total = score.procedural + score.neural + score.hybrid + if (total === 0) return null // ambiguous β€” need LLM + + // Hybrid wins if it has ANY matches (it's more specific) + if (score.hybrid >= 2) { + return { + strategy: "hybrid", + confidence: Math.min(0.85, 0.6 + score.hybrid * 0.1), + reasoning: "Request combines neural generation with Blender post-processing (rigging, animation, export).", + classificationMethod: "keyword", + } + } + + // Clear procedural winner + if (score.procedural > 0 && score.neural === 0) { + return { + strategy: "procedural", + confidence: Math.min(0.95, 0.7 + score.procedural * 0.05), + reasoning: "Request involves geometric, architectural, or scene-editing operations best handled by Blender Python.", + classificationMethod: "keyword", + } + } + + // Clear neural winner + if (score.neural > 0 && score.procedural === 0) { + return { + strategy: "neural", + confidence: Math.min(0.9, 0.65 + score.neural * 0.08), + reasoning: "Request involves organic shapes, characters, or photorealistic assets best suited for neural 3D generation.", + suggestedProviders: ["hunyuan-shape", "trellis"], + classificationMethod: "keyword", + } + } + + // Mixed signals β€” if procedural dominates, favor procedural (cheaper, faster) + if (score.procedural >= score.neural * 2) { + return { + strategy: "procedural", + confidence: 0.6, + reasoning: "Request has mixed signals but leans procedural. Blender Python is preferred for precision.", + classificationMethod: "keyword", + } + } + + // Mixed but neural leans stronger + if (score.neural >= score.procedural * 2) { + return { + strategy: "neural", + confidence: 0.6, + reasoning: "Request has mixed signals but leans toward organic/complex shapes suited for neural generation.", + suggestedProviders: ["hunyuan-shape"], + classificationMethod: "keyword", + } + } + + // Truly ambiguous β€” fall through to LLM + return null +} + +// --------------------------------------------------------------------------- +// LLM-based classification (fallback for ambiguous requests) +// --------------------------------------------------------------------------- + +async function classifyWithLLM( + userRequest: string, + sceneContext?: string +): Promise { + // Dynamic import to avoid circular dependencies + const { generateGeminiResponse } = await import("@/lib/gemini") + + const systemPrompt = `You are a 3D generation strategy classifier for ModelForge. +Classify the user's request into exactly ONE strategy: + +- "procedural": For geometric shapes, architecture, furniture, mechanical parts, scene edits, lighting, camera, materials. Generated via Blender Python scripting. +- "neural": For organic characters, animals, sculptures, plants, photorealistic assets. Generated by neural 3D models (Hunyuan, TRELLIS). +- "hybrid": When the request needs neural generation PLUS Blender post-processing like rigging, animation, retopology, or game-ready export. + +When in doubt, prefer "procedural" β€” it's faster, cheaper, and more controllable. + +Respond with ONLY a JSON object: +{"strategy": "procedural"|"neural"|"hybrid", "confidence": 0.0-1.0, "reasoning": "one sentence", "suggestedProviders": ["hunyuan-shape"|"hunyuan-paint"|"trellis"|"yvo3d"]} + +suggestedProviders should be empty [] for procedural strategy.` + + const userPrompt = sceneContext + ? `Scene context: ${sceneContext}\n\nUser request: ${userRequest}` + : `User request: ${userRequest}` + + try { + const result = await generateGeminiResponse({ + messages: [{ role: "user", content: userPrompt }], + systemPrompt, + temperature: 0, + maxOutputTokens: 256, + responseMimeType: "application/json", + }) + + const parsed = JSON.parse(result.text) as { + strategy: Strategy + confidence: number + reasoning: string + suggestedProviders?: ProviderSlug[] + } + + // Validate + const validStrategies: Strategy[] = ["procedural", "neural", "hybrid"] + if (!validStrategies.includes(parsed.strategy)) { + parsed.strategy = "procedural" + } + parsed.confidence = Math.max(0, Math.min(1, parsed.confidence ?? 0.5)) + + return { + strategy: parsed.strategy, + confidence: parsed.confidence, + reasoning: parsed.reasoning || "Classified by LLM.", + suggestedProviders: parsed.suggestedProviders, + classificationMethod: "llm", + } + } catch (error) { + console.warn("LLM strategy classification failed, defaulting to procedural:", error) + return { + strategy: "procedural", + confidence: 0.5, + reasoning: "LLM classification failed; defaulting to procedural (safest option).", + classificationMethod: "llm", + } + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export interface ClassifyOptions { + /** User's manual override β€” skips all classification */ + override?: StrategyOverride + /** Current scene context for LLM classification */ + sceneContext?: string + /** Skip LLM fallback β€” only use keyword classification (default: false) */ + keywordOnly?: boolean +} + +/** + * Classify a user request into the best generation strategy. + * + * Classification pipeline: + * 1. If user override is set β†’ return immediately + * 2. Keyword pattern matching (fast, covers ~80% of requests) + * 3. LLM classification fallback (for ambiguous requests) + * + * @param userRequest - The user's natural language request + * @param options - Classification options + * @returns StrategyDecision with strategy, confidence, and reasoning + */ +export async function classifyStrategy( + userRequest: string, + options: ClassifyOptions = {} +): Promise { + // 1. User override β€” always wins + if (options.override) { + return { + strategy: options.override.strategy, + confidence: 1.0, + reasoning: `User manually selected "${options.override.strategy}" strategy.`, + suggestedProviders: options.override.providers, + classificationMethod: "user_override", + } + } + + // 2. Keyword-based classification + const score = scoreKeywords(userRequest) + const keywordResult = decideFromKeywords(score) + + if (keywordResult && keywordResult.confidence >= 0.6) { + return keywordResult + } + + // 3. LLM fallback (unless disabled) + if (!options.keywordOnly) { + return classifyWithLLM(userRequest, options.sceneContext) + } + + // 4. Keyword-only mode with low confidence β€” default to procedural + return keywordResult ?? { + strategy: "procedural", + confidence: 0.4, + reasoning: "Could not confidently classify; defaulting to procedural generation.", + classificationMethod: "keyword", + } +} diff --git a/lib/orchestration/strategy-types.ts b/lib/orchestration/strategy-types.ts new file mode 100644 index 0000000..1a668b3 --- /dev/null +++ b/lib/orchestration/strategy-types.ts @@ -0,0 +1,41 @@ +import type { ProviderSlug } from "@/lib/neural/types" + +// --------------------------------------------------------------------------- +// Strategy Classification Types +// --------------------------------------------------------------------------- + +/** + * The three generation strategies available in ModelForge. + * + * - `procedural` β€” Blender Python code-gen for geometric, architectural, parametric objects + * - `neural` β€” Neural 3D generation for organic, complex, photorealistic assets + * - `hybrid` β€” Neural geometry β†’ Blender post-processing (retopo, rig, animate, export) + */ +export type Strategy = "procedural" | "neural" | "hybrid" + +/** + * Result of the strategy classification. + */ +export interface StrategyDecision { + /** Which pipeline to invoke */ + strategy: Strategy + /** 0.0–1.0 confidence in the classification */ + confidence: number + /** Human-readable reasoning for the decision */ + reasoning: string + /** Recommended providers when strategy is neural/hybrid */ + suggestedProviders?: ProviderSlug[] + /** How the decision was made */ + classificationMethod: "keyword" | "llm" | "user_override" +} + +/** + * User-level override: lets users explicitly pick a strategy from the UI. + * When set, the router skips classification entirely. + */ +export interface StrategyOverride { + /** Force a specific strategy */ + strategy: Strategy + /** Optional: force specific neural providers */ + providers?: ProviderSlug[] +} diff --git a/lib/orchestration/types.ts b/lib/orchestration/types.ts index 620bc7d..fcc3f9a 100644 --- a/lib/orchestration/types.ts +++ b/lib/orchestration/types.ts @@ -1,3 +1,6 @@ +import type { StrategyDecision } from "./strategy-types" +import type { WorkflowProposal, WorkflowStepStatus } from "./workflow-types" + export interface PlanStep { stepNumber: number action: string @@ -96,6 +99,13 @@ interface AgentVision extends AgentEventBase { type: "agent:vision"; stepIndex?: interface AgentAudit extends AgentEventBase { type: "agent:audit"; success: boolean; reason?: string } interface AgentComplete extends AgentEventBase { type: "agent:complete"; success: boolean; completedCount: number; failedCount: number } interface AgentCodeGeneration extends AgentEventBase { type: "agent:code_generation"; stepIndex: number; description: string } +interface AgentVisualAnalysis extends AgentEventBase { type: "agent:visual_analysis"; iteration: number; description: string } +interface AgentVisualCorrection extends AgentEventBase { type: "agent:visual_correction"; iteration: number; description: string; issues: string[] } +interface AgentNeuralGeneration extends AgentEventBase { type: "agent:neural_generation"; provider: string; stage: string; status: string; progress?: number } +interface AgentHybridPipeline extends AgentEventBase { type: "agent:hybrid_pipeline"; stagesCompleted: number; stagesTotal: number; currentStage: string } +interface AgentStrategyClassification extends AgentEventBase { type: "agent:strategy_classification"; strategy: string; confidence: number; reasoning: string; method: string } +interface AgentWorkflowProposal extends AgentEventBase { type: "agent:workflow_proposal"; proposal: WorkflowProposal } +interface AgentWorkflowStepUpdate extends AgentEventBase { type: "agent:workflow_step_update"; stepId: string; status: WorkflowStepStatus; message?: string; error?: string; durationMs?: number } /** * Real-time stream event types sent during agent execution @@ -113,6 +123,13 @@ export type AgentStreamEvent = | AgentAudit | AgentComplete | AgentCodeGeneration + | AgentVisualAnalysis + | AgentVisualCorrection + | AgentNeuralGeneration + | AgentHybridPipeline + | AgentStrategyClassification + | AgentWorkflowProposal + | AgentWorkflowStepUpdate export interface PlanningMetadata { planSummary: string @@ -127,4 +144,5 @@ export interface PlanningMetadata { analysis?: PlanAnalysis researchSummary?: string researchSources?: ResearchSource[] + strategyDecision?: StrategyDecision } diff --git a/lib/orchestration/workflow-advisor.ts b/lib/orchestration/workflow-advisor.ts new file mode 100644 index 0000000..59c8bde --- /dev/null +++ b/lib/orchestration/workflow-advisor.ts @@ -0,0 +1,388 @@ +/** + * Workflow Advisor β€” Guided Human-in-the-Loop 3D Creation + * + * Generates per-step tool recommendations for complex 3D creation tasks. + * Each step tells the user: + * - Which tool is best (neural model, Blender agent, or manual) + * - WHY that tool is the best choice + * - Alternative approaches they could use instead + * + * Only invoked when the strategy router classifies a request as "neural" or "hybrid". + * Procedural requests bypass this entirely and use the existing auto-pilot planner. + */ + +import type { Strategy } from "./strategy-types" +import type { + WorkflowProposal, + WorkflowStep, + WorkflowTool, + WorkflowCategory, +} from "./workflow-types" + +// --------------------------------------------------------------------------- +// Tool recommendation knowledge base +// --------------------------------------------------------------------------- + +interface ToolRecommendation { + tool: WorkflowTool + reasoning: string + alternatives: WorkflowTool[] + estimatedDuration: string + neuralProvider?: string + blenderAction?: string +} + +/** + * Static knowledge base mapping categories to their best tool. + * Used as a fallback and to augment LLM proposals. + */ +const CATEGORY_DEFAULTS: Record = { + geometry: { + tool: "neural", + reasoning: + "Neural models excel at generating complex organic shapes from text/image descriptions. Much faster than manual modeling for the initial base mesh.", + alternatives: ["blender_agent", "manual"], + estimatedDuration: "~1-3 min", + neuralProvider: "hunyuan-shape", + }, + topology: { + tool: "blender_agent", + reasoning: + "Blender's built-in retopology tools (Voxel Remesh, Quadriflow) produce clean quad meshes efficiently. The agent uses proven RAG scripts.", + alternatives: ["manual"], + estimatedDuration: "~15-30s", + blenderAction: "execute_code", + }, + uv: { + tool: "blender_agent", + reasoning: + "Auto UV unwrapping via Smart Project or Lightmap Pack produces good results for most models. Manual UV only needed for hero assets.", + alternatives: ["manual"], + estimatedDuration: "~10-20s", + blenderAction: "execute_code", + }, + texturing: { + tool: "neural", + reasoning: + "Neural texture models (Hunyuan Paint, YVO3D) generate cohesive PBR textures across the entire model. Best for organic surfaces.", + alternatives: ["blender_agent", "manual"], + estimatedDuration: "~1-2 min", + neuralProvider: "hunyuan-paint", + }, + rigging: { + tool: "blender_agent", + reasoning: + "Rigify auto-rigging in Blender is production-proven for bipeds and quadrupeds. The agent configures metarigs and applies automatic weights.", + alternatives: ["manual"], + estimatedDuration: "~20-40s", + blenderAction: "execute_code", + }, + animation: { + tool: "blender_agent", + reasoning: + "Procedural animation (orbit, walk cycles, keyframe insertion) is fast and precise via Python. Manual keyframing for custom performances.", + alternatives: ["manual"], + estimatedDuration: "~15-30s", + blenderAction: "execute_code", + }, + lighting: { + tool: "blender_agent", + reasoning: + "Lighting recipes (3-point, studio, HDRI) are handled via proven Python scripts. Always procedural.", + alternatives: ["manual"], + estimatedDuration: "~10s", + blenderAction: "execute_code", + }, + export: { + tool: "blender_agent", + reasoning: + "Export with LOD generation and format presets (glTF, FBX, USD) is fully automated. The agent validates the output before exporting.", + alternatives: ["manual"], + estimatedDuration: "~10-20s", + blenderAction: "execute_code", + }, + composition: { + tool: "blender_agent", + reasoning: + "Scene composition (layouts, pedestals, backdrops) is best handled by the Blender agent using procedural placement scripts.", + alternatives: ["manual"], + estimatedDuration: "~10-20s", + blenderAction: "execute_code", + }, + other: { + tool: "manual", + reasoning: + "This step requires manual artistic judgment. Use Blender's tools directly for the best results.", + alternatives: ["blender_agent"], + estimatedDuration: "varies", + }, +} + +// --------------------------------------------------------------------------- +// LLM-powered workflow generation +// --------------------------------------------------------------------------- + +/** + * Raw LLM response shape for workflow analysis. + */ +interface LLMWorkflowResponse { + title: string + steps: Array<{ + title: string + description: string + category: WorkflowCategory + recommendedTool: WorkflowTool + toolReasoning: string + alternativeTools: WorkflowTool[] + estimatedDuration: string + requiresPreviousStep: boolean + tips?: string + neuralProvider?: string + }> + overallTips: string[] +} + +const WORKFLOW_SYSTEM_PROMPT = `You are a 3D workflow advisor for ModelForge. Given a user's creation request, generate a step-by-step workflow where each step recommends the BEST tool. + +Available tools: +- "neural": AI model generation (best for organic geometry, complex textures). Providers: hunyuan-shape (geometry), hunyuan-paint (PBR textures), trellis (geometry+texture) +- "blender_agent": Automated Blender Python scripts (best for retopology, UV, rigging, animation, lighting, scene edits, export) +- "manual": User does it themselves in Blender (best for artistic fine-tuning, custom sculpting, precise adjustments) + +Category values: geometry, topology, uv, texturing, rigging, animation, lighting, export, composition, other + +Rules: +1. Keep steps between 3-8 depending on complexity +2. For geometry of organic/creature/character shapes β†’ recommend "neural" +3. For geometry of architectural/mechanical shapes β†’ recommend "blender_agent" +4. Retopology, UV unwrap, rigging, animation, lighting, export β†’ always "blender_agent" +5. Texturing of organic models β†’ "neural", texturing with specific PBR materials β†’ "blender_agent" +6. Only recommend "manual" for steps requiring artistic judgment (fine detail sculpting, weight paint adjustments) +7. Set requiresPreviousStep=true when a step depends on the output of the prior step +8. Include practical tips for each step +9. neuralProvider is required when recommendedTool is "neural" + +Respond with ONLY a JSON object matching this schema: +{ + "title": "string β€” workflow title", + "steps": [ + { + "title": "string", + "description": "string β€” what this step does", + "category": "geometry|topology|uv|texturing|rigging|animation|lighting|export|composition|other", + "recommendedTool": "neural|blender_agent|manual", + "toolReasoning": "string β€” WHY this tool is best", + "alternativeTools": ["neural"|"blender_agent"|"manual"], + "estimatedDuration": "~Xs or ~Xmin", + "requiresPreviousStep": true|false, + "tips": "optional string", + "neuralProvider": "hunyuan-shape|hunyuan-paint|trellis|yvo3d β€” required when tool is neural" + } + ], + "overallTips": ["string"] +}` + +/** + * Generate a workflow proposal using LLM analysis of the user's request. + */ +async function generateWithLLM( + userRequest: string, + strategy: Strategy, + sceneContext?: string +): Promise { + const { generateGeminiResponse } = await import("@/lib/gemini") + + const userPrompt = [ + `Strategy classification: ${strategy}`, + sceneContext ? `Current scene: ${sceneContext}` : null, + `User request: ${userRequest}`, + ] + .filter(Boolean) + .join("\n\n") + + const result = await generateGeminiResponse({ + messages: [{ role: "user", content: userPrompt }], + systemPrompt: WORKFLOW_SYSTEM_PROMPT, + temperature: 0.1, + maxOutputTokens: 2048, + responseMimeType: "application/json", + }) + + const parsed = JSON.parse(result.text) as LLMWorkflowResponse + + // Validate categories and tools + const validTools: WorkflowTool[] = ["blender_agent", "neural", "manual"] + const validCategories: WorkflowCategory[] = [ + "geometry", "topology", "uv", "texturing", "rigging", + "animation", "lighting", "export", "composition", "other", + ] + + for (const step of parsed.steps) { + if (!validTools.includes(step.recommendedTool)) { + step.recommendedTool = "blender_agent" + } + if (!validCategories.includes(step.category)) { + step.category = "other" + } + // Ensure alternatives don't include the recommended tool + step.alternativeTools = (step.alternativeTools ?? []).filter( + (t) => validTools.includes(t) && t !== step.recommendedTool + ) + } + + return parsed +} + +// --------------------------------------------------------------------------- +// Fallback: deterministic workflow template +// --------------------------------------------------------------------------- + +/** + * Generates a reasonable workflow without LLM, based on the strategy. + * Used when the LLM call fails. + */ +function generateFallbackWorkflow( + userRequest: string, + strategy: Strategy +): LLMWorkflowResponse { + const steps: LLMWorkflowResponse["steps"] = [] + + if (strategy === "neural" || strategy === "hybrid") { + steps.push({ + title: "Generate Base Mesh", + description: `Use a neural 3D model to generate the initial geometry for: "${userRequest}"`, + category: "geometry", + recommendedTool: "neural", + toolReasoning: CATEGORY_DEFAULTS.geometry.reasoning, + alternativeTools: ["manual"], + estimatedDuration: CATEGORY_DEFAULTS.geometry.estimatedDuration, + requiresPreviousStep: false, + tips: "You can also upload a reference image for image-to-3D generation.", + neuralProvider: "hunyuan-shape", + }) + } + + if (strategy === "hybrid") { + steps.push( + { + title: "Clean Up Topology", + description: "Apply automatic retopology to create clean quad-based geometry suitable for deformation and export.", + category: "topology", + recommendedTool: "blender_agent", + toolReasoning: CATEGORY_DEFAULTS.topology.reasoning, + alternativeTools: ["manual"], + estimatedDuration: CATEGORY_DEFAULTS.topology.estimatedDuration, + requiresPreviousStep: true, + }, + { + title: "UV Unwrap", + description: "Automatically generate UV maps for texturing.", + category: "uv", + recommendedTool: "blender_agent", + toolReasoning: CATEGORY_DEFAULTS.uv.reasoning, + alternativeTools: ["manual"], + estimatedDuration: CATEGORY_DEFAULTS.uv.estimatedDuration, + requiresPreviousStep: true, + }, + { + title: "Apply Textures", + description: "Generate PBR textures using a neural texture model, or apply materials using the Blender agent.", + category: "texturing", + recommendedTool: "neural", + toolReasoning: CATEGORY_DEFAULTS.texturing.reasoning, + alternativeTools: ["blender_agent", "manual"], + estimatedDuration: CATEGORY_DEFAULTS.texturing.estimatedDuration, + requiresPreviousStep: true, + neuralProvider: "hunyuan-paint", + }, + { + title: "Export", + description: "Export the final model in glTF/FBX format with validation.", + category: "export", + recommendedTool: "blender_agent", + toolReasoning: CATEGORY_DEFAULTS.export.reasoning, + alternativeTools: ["manual"], + estimatedDuration: CATEGORY_DEFAULTS.export.estimatedDuration, + requiresPreviousStep: true, + } + ) + } + + return { + title: `Workflow: ${userRequest.slice(0, 60)}`, + steps, + overallTips: [ + "You can skip any step and do it manually in Blender.", + "Neural generation works best with descriptive, detailed prompts.", + "The Blender agent uses battle-tested RAG scripts for post-processing.", + ], + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export interface WorkflowAdvisorOptions { + /** Current scene context for better recommendations */ + sceneContext?: string +} + +/** + * Generate a workflow proposal for a user's creation request. + * + * @param userRequest - The user's natural language request + * @param strategy - The classified strategy (should be "neural" or "hybrid") + * @param options - Additional options (scene context, etc.) + * @returns A WorkflowProposal with per-step tool recommendations + */ +export async function generateWorkflowProposal( + userRequest: string, + strategy: Strategy, + options: WorkflowAdvisorOptions = {} +): Promise { + let llmResponse: LLMWorkflowResponse + + try { + llmResponse = await generateWithLLM(userRequest, strategy, options.sceneContext) + } catch (error) { + console.warn("Workflow LLM generation failed, using fallback:", error) + llmResponse = generateFallbackWorkflow(userRequest, strategy) + } + + // Convert LLM response to WorkflowStep[] with status tracking + const steps: WorkflowStep[] = llmResponse.steps.map((step, index) => { + const categoryDefault = CATEGORY_DEFAULTS[step.category] ?? CATEGORY_DEFAULTS.other + + return { + id: `step-${index + 1}-${step.category}`, + stepNumber: index + 1, + title: step.title, + description: step.description, + recommendedTool: step.recommendedTool, + toolReasoning: step.toolReasoning || categoryDefault.reasoning, + alternativeTools: step.alternativeTools.length > 0 + ? step.alternativeTools + : categoryDefault.alternatives, + category: step.category, + estimatedDuration: step.estimatedDuration || categoryDefault.estimatedDuration, + requiresPreviousStep: step.requiresPreviousStep, + tips: step.tips, + status: "pending", + neuralProvider: step.neuralProvider ?? categoryDefault.neuralProvider, + blenderAction: categoryDefault.blenderAction, + } + }) + + const proposal: WorkflowProposal = { + id: `workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + title: llmResponse.title, + strategy, + steps, + overallTips: llmResponse.overallTips ?? [], + userRequest, + createdAt: new Date().toISOString(), + } + + return proposal +} diff --git a/lib/orchestration/workflow-types.ts b/lib/orchestration/workflow-types.ts new file mode 100644 index 0000000..ad60233 --- /dev/null +++ b/lib/orchestration/workflow-types.ts @@ -0,0 +1,115 @@ +/** + * Guided Workflow System β€” Type Definitions + * + * Provides types for the human-in-the-loop workflow where each creation step + * gets a tool recommendation and the user controls execution. + */ + +import type { Strategy } from "./strategy-types" + +// ─── Tool classification for each step ──────────────────────────── + +export type WorkflowTool = "blender_agent" | "neural" | "manual" + +export type WorkflowCategory = + | "geometry" + | "topology" + | "uv" + | "texturing" + | "rigging" + | "animation" + | "lighting" + | "export" + | "composition" + | "other" + +// ─── Step status lifecycle ──────────────────────────────────────── + +export type WorkflowStepStatus = + | "pending" // Not yet acted on + | "running" // Currently executing + | "completed" // Successfully executed + | "failed" // Execution failed + | "skipped" // User chose to skip + | "manual" // User handled it manually + +export type WorkflowStepAction = "execute" | "skip" | "manual_done" + +// ─── Individual workflow step ───────────────────────────────────── + +export interface WorkflowStep { + /** Unique step ID (e.g. "step-1-geometry") */ + id: string + stepNumber: number + /** Short title (e.g. "Base Mesh Generation") */ + title: string + /** What this step does (e.g. "Generate the initial dragon geometry from text description") */ + description: string + /** Best tool for this step */ + recommendedTool: WorkflowTool + /** WHY this tool is the best choice */ + toolReasoning: string + /** Other valid tools the user could use instead */ + alternativeTools: WorkflowTool[] + /** Creation category */ + category: WorkflowCategory + /** Estimated time (e.g. "~30s", "~2min") */ + estimatedDuration: string + /** Whether the previous step must complete first */ + requiresPreviousStep: boolean + /** Pro tip for this step */ + tips?: string + /** Current status */ + status: WorkflowStepStatus + /** Neural provider slug if tool is "neural" (e.g. "hunyuan-shape") */ + neuralProvider?: string + /** Blender agent action if tool is "blender_agent" (e.g. "execute_code") */ + blenderAction?: string + /** Error message if step failed */ + error?: string + /** Output path if step produced a file */ + outputPath?: string +} + +// ─── Full workflow proposal ─────────────────────────────────────── + +export interface WorkflowProposal { + /** Unique workflow ID */ + id: string + /** Human-readable title (e.g. "Dragon Character Creation Workflow") */ + title: string + /** Strategy from the router */ + strategy: Strategy + /** Ordered list of recommended steps */ + steps: WorkflowStep[] + /** General tips for the overall workflow */ + overallTips: string[] + /** The original user request */ + userRequest: string + /** Timestamp */ + createdAt: string +} + +// ─── Step execution request (from UI to API) ────────────────────── + +export interface WorkflowStepRequest { + conversationId: string + workflowId: string + stepId: string + action: WorkflowStepAction +} + +// ─── Step execution result (API to UI) ──────────────────────────── + +export interface WorkflowStepResult { + stepId: string + status: WorkflowStepStatus + /** Result message */ + message?: string + /** Error details if failed */ + error?: string + /** Output file path if applicable */ + outputPath?: string + /** Execution time in ms */ + durationMs?: number +} diff --git a/lib/stripe.ts b/lib/stripe.ts index 47ddc85..ed0acfc 100644 --- a/lib/stripe.ts +++ b/lib/stripe.ts @@ -18,7 +18,7 @@ export const PRICING_TIERS = { limits: { maxProjects: 1, dailyRequests: 5, - modelAccess: 'gemini-2.5-pro' + modelAccess: 'gemini-3.1-pro-preview' } }, STARTER: { diff --git a/package-lock.json b/package-lock.json index 4f616e0..e14f68f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "autoprefixer": "^10.4.0", + "baseline-browser-mapping": "^2.10.0", "eslint": "^8.57.0", "eslint-config-next": "^16.0.10", "postcss": "^8.4.0", @@ -3202,12 +3203,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", - "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, + "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bidi-js": { diff --git a/package.json b/package.json index e42d9eb..61c4dc4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "test:memory": "tsx scripts/test-memory.ts", "test:rag": "tsx scripts/test-rag.ts", "analyze:orchestration": "node scripts/analyze-orchestration.mjs", - "ingest:blender": "tsx scripts/ingest-blender-docs.ts" + "ingest:blender": "tsx scripts/ingest-blender-docs.ts", + "mock:neural": "tsx scripts/mock-neural-server.ts" }, "dependencies": { "@google/generative-ai": "^0.24.1", @@ -52,6 +53,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "autoprefixer": "^10.4.0", + "baseline-browser-mapping": "^2.10.0", "eslint": "^8.57.0", "eslint-config-next": "^16.0.10", "postcss": "^8.4.0", diff --git a/public/downloads/modelforge-addon.py b/public/downloads/modelforge-addon.py index 8bb2532..b7e7800 100644 --- a/public/downloads/modelforge-addon.py +++ b/public/downloads/modelforge-addon.py @@ -35,9 +35,6 @@ REQ_HEADERS = requests.utils.default_headers() REQ_HEADERS.update({"User-Agent": "modelforge-blender"}) -# Default timeout for HTTP requests (seconds) -DEFAULT_TIMEOUT = 30 - class BlenderMCPServer: def __init__(self, host='localhost', port=9876): self.host = host @@ -450,19 +447,23 @@ def get_all_object_info(self, max_objects=50, start_index=0): def get_viewport_screenshot(self, max_size=800, filepath=None, format="png"): """ - Capture a screenshot of the current 3D viewport and save it to the specified path. + Capture a screenshot of the current 3D viewport. Parameters: - max_size: Maximum size in pixels for the largest dimension of the image - - filepath: Path where to save the screenshot file + - filepath: Optional path to save the screenshot file. If None, returns + the image as base64-encoded data directly. - format: Image format (png, jpg, etc.) - Returns success/error status + Returns: + - If filepath: {success, width, height, filepath} + - If no filepath: {image (base64), width, height, format} """ - try: - if not filepath: - return {"error": "No filepath provided"} + import os + import tempfile + import base64 + try: # Find the active 3D viewport area = None for a in bpy.context.screen.areas: @@ -473,6 +474,13 @@ def get_viewport_screenshot(self, max_size=800, filepath=None, format="png"): if not area: return {"error": "No 3D viewport found"} + # Determine file path β€” use temp if none provided + return_base64 = filepath is None + if return_base64: + tmp = tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) + filepath = tmp.name + tmp.close() + # Take screenshot with proper context override with bpy.context.temp_override(area=area): bpy.ops.screen.screenshot_area(filepath=filepath) @@ -495,14 +503,36 @@ def get_viewport_screenshot(self, max_size=800, filepath=None, format="png"): # Cleanup Blender image data bpy.data.images.remove(img) - return { - "success": True, - "width": width, - "height": height, - "filepath": filepath - } + if return_base64: + # Read the file and encode as base64 + with open(filepath, "rb") as f: + image_data = base64.b64encode(f.read()).decode("utf-8") + # Clean up temp file + try: + os.remove(filepath) + except OSError: + pass + return { + "image": image_data, + "width": width, + "height": height, + "format": format, + } + else: + return { + "success": True, + "width": width, + "height": height, + "filepath": filepath + } except Exception as e: + # Clean up temp file on error + if return_base64 and filepath: + try: + os.remove(filepath) + except OSError: + pass return {"error": str(e)} def execute_code(self, code): @@ -530,7 +560,7 @@ def get_polyhaven_categories(self, asset_type): if asset_type not in ["hdris", "textures", "models", "all"]: return {"error": f"Invalid asset type: {asset_type}. Must be one of: hdris, textures, models, all"} - response = requests.get(f"https://api.polyhaven.com/categories/{asset_type}", headers=REQ_HEADERS, timeout=DEFAULT_TIMEOUT) + response = requests.get(f"https://api.polyhaven.com/categories/{asset_type}", headers=REQ_HEADERS) if response.status_code == 200: return {"categories": response.json()} else: @@ -552,7 +582,7 @@ def search_polyhaven_assets(self, asset_type=None, categories=None): if categories: params["categories"] = categories - response = requests.get(url, params=params, headers=REQ_HEADERS, timeout=DEFAULT_TIMEOUT) + response = requests.get(url, params=params, headers=REQ_HEADERS) if response.status_code == 200: # Limit the response size to avoid overwhelming Blender assets = response.json() @@ -572,7 +602,7 @@ def search_polyhaven_assets(self, asset_type=None, categories=None): def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_format=None): try: # First get the files information - files_response = requests.get(f"https://api.polyhaven.com/files/{asset_id}", headers=REQ_HEADERS, timeout=DEFAULT_TIMEOUT) + files_response = requests.get(f"https://api.polyhaven.com/files/{asset_id}", headers=REQ_HEADERS) if files_response.status_code != 200: return {"error": f"Failed to get asset files: {files_response.status_code}"} @@ -1000,7 +1030,27 @@ def set_texture(self, object_name, texture_id): links.new(mapping.outputs['Vector'], tex_node.inputs['Vector']) - # Store reference for second-pass wiring (avoid double-linking) + # Connect to appropriate input on Principled BSDF + if map_type.lower() in ['color', 'diffuse', 'albedo']: + links.new(tex_node.outputs['Color'], principled.inputs['Base Color']) + elif map_type.lower() in ['roughness', 'rough']: + links.new(tex_node.outputs['Color'], principled.inputs['Roughness']) + elif map_type.lower() in ['metallic', 'metalness', 'metal']: + links.new(tex_node.outputs['Color'], principled.inputs['Metallic']) + elif map_type.lower() in ['normal', 'nor', 'dx', 'gl']: + # Add normal map node + normal_map = nodes.new(type='ShaderNodeNormalMap') + normal_map.location = (x_pos + 200, y_pos) + links.new(tex_node.outputs['Color'], normal_map.inputs['Color']) + links.new(normal_map.outputs['Normal'], principled.inputs['Normal']) + elif map_type.lower() in ['displacement', 'disp', 'height']: + # Add displacement node + disp_node = nodes.new(type='ShaderNodeDisplacement') + disp_node.location = (x_pos + 200, y_pos - 200) + disp_node.inputs['Scale'].default_value = 0.1 # Reduce displacement strength + links.new(tex_node.outputs['Color'], disp_node.inputs['Height']) + links.new(disp_node.outputs['Displacement'], output.inputs['Displacement']) + y_pos -= 250 # Second pass: Connect nodes with proper handling for special cases @@ -1225,7 +1275,7 @@ def create_rodin_job(self, *args, **kwargs): case "FAL_AI": return self.create_rodin_job_fal_ai(*args, **kwargs) case _: - return {"error": f"Unknown Hyper3D Rodin mode: {bpy.context.scene.blendermcp_hyper3d_mode}"} + return f"Error: Unknown Hyper3D Rodin mode!" def create_rodin_job_main_site( self, @@ -1317,9 +1367,8 @@ def poll_rodin_job_status_fal_ai(self, request_id: str): response = requests.get( f"https://queue.fal.run/fal-ai/hyper3d/requests/{request_id}/status", headers={ - "Authorization": f"Key {bpy.context.scene.blendermcp_hyper3d_api_key}", + "Authorization": f"KEY {bpy.context.scene.blendermcp_hyper3d_api_key}", }, - timeout=30, ) data = response.json() return data @@ -1398,7 +1447,7 @@ def import_generated_asset(self, *args, **kwargs): case "FAL_AI": return self.import_generated_asset_fal_ai(*args, **kwargs) case _: - return {"error": f"Unknown Hyper3D Rodin mode: {bpy.context.scene.blendermcp_hyper3d_mode}"} + return f"Error: Unknown Hyper3D Rodin mode!" def import_generated_asset_main_site(self, task_uuid: str, name: str): """Fetch the generated asset, import into blender""" @@ -1448,8 +1497,6 @@ def import_generated_asset_main_site(self, task_uuid: str, name: str): filepath=temp_file.name, mesh_name=name ) - if obj is None: - return {"succeed": False, "error": "Import succeeded but no mesh object was found"} result = { "name": obj.name, "type": obj.type, @@ -1467,13 +1514,6 @@ def import_generated_asset_main_site(self, task_uuid: str, name: str): } except Exception as e: return {"succeed": False, "error": str(e)} - finally: - # Clean up temp file - try: - if temp_file and os.path.isfile(temp_file.name): - os.unlink(temp_file.name) - except OSError: - pass def import_generated_asset_fal_ai(self, request_id: str, name: str): """Fetch the generated asset, import into blender""" @@ -1515,8 +1555,6 @@ def import_generated_asset_fal_ai(self, request_id: str, name: str): filepath=temp_file.name, mesh_name=name ) - if obj is None: - return {"succeed": False, "error": "Import succeeded but no mesh object was found"} result = { "name": obj.name, "type": obj.type, @@ -1534,13 +1572,6 @@ def import_generated_asset_fal_ai(self, request_id: str, name: str): } except Exception as e: return {"succeed": False, "error": str(e)} - finally: - # Clean up temp file - try: - if temp_file and os.path.isfile(temp_file.name): - os.unlink(temp_file.name) - except OSError: - pass #endregion #region Sketchfab API @@ -1585,9 +1616,9 @@ def get_sketchfab_status(self): "message": f"Error testing Sketchfab API key: {str(e)}" } - # If api_key was present, the block above already returned. - # This handles: enabled with no key, or disabled entirely. - if enabled and not api_key: + if enabled and api_key: + return {"enabled": True, "message": "Sketchfab integration is enabled and ready to use."} + elif enabled and not api_key: return { "enabled": False, "message": """Sketchfab integration is currently enabled, but API key is not given. To enable it: diff --git a/scripts/generate-training-data.ts b/scripts/generate-training-data.ts new file mode 100644 index 0000000..44316bb --- /dev/null +++ b/scripts/generate-training-data.ts @@ -0,0 +1,284 @@ +/** + * Generate Training Data for Blender Code Generation Fine-Tuning + * + * Reads all 124 RAG scripts from data/blender-scripts/, + * extracts function-level code blocks, and generates + * instructionβ†’output training pairs in JSONL format. + * + * Output: training/training_data.jsonl + * + * Usage: npx tsx scripts/generate-training-data.ts + */ + +import fs from "fs"; +import path from "path"; + +const SCRIPTS_DIR = path.join(process.cwd(), "data", "blender-scripts"); +const OUTPUT_FILE = path.join(process.cwd(), "training", "training_data.jsonl"); + +interface TrainingPair { + instruction: string; + output: string; + category: string; + source_file: string; + pair_type: "function" | "full_script" | "pattern" | "pitfall"; +} + +// ─── Recursive file walker ───────────────────────────── +function findPyFiles(dir: string, files: string[] = []): string[] { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) findPyFiles(full, files); + else if (entry.name.endsWith(".py")) files.push(full); + } + return files; +} + +// ─── Extract category from file path ─────────────────── +function getCategory(filePath: string): string { + const rel = path.relative(SCRIPTS_DIR, filePath); + const parts = rel.split(path.sep); + if (parts.length === 1) return "utility"; + parts.pop(); + return parts.join("/"); +} + +// ─── Parse module-level docstring ────────────────────── +function parseModuleDocstring(content: string): { + title: string; + description: string; + rawDocstring: string; +} { + const match = content.match(/^"""([\s\S]*?)"""/); + if (!match) return { title: "", description: "", rawDocstring: "" }; + + const raw = match[1].trim(); + + // Try JSON metadata format (task scripts) + try { + const json = JSON.parse(raw); + return { + title: json.title || "", + description: json.description || "", + rawDocstring: raw, + }; + } catch { + // Plain text docstring (utility scripts) + const lines = raw.split("\n").map((l) => l.trim()); + const title = lines[0]?.replace(/[=\-─]/g, "").trim() || ""; + const descLines = lines.filter( + (l) => + l && + !l.startsWith("Category:") && + !l.startsWith("Blender:") && + !l.startsWith("Source:") && + !l.startsWith("===") && + !l.startsWith("---") && + l !== title + ); + return { + title, + description: descLines.slice(0, 3).join(" "), + rawDocstring: raw, + }; + } +} + +// ─── Extract Python functions with docstrings ────────── +interface FunctionBlock { + name: string; + signature: string; + docstring: string; + body: string; + fullCode: string; +} + +function extractFunctions(content: string): FunctionBlock[] { + const functions: FunctionBlock[] = []; + const lines = content.split("\n"); + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + const defMatch = line.match(/^def\s+(\w+)\s*\(([\s\S]*?)(?:\).*:)/); + + if (defMatch) { + const name = defMatch[1]; + + // Collect full signature (may span lines) + let sigLines = [line]; + let j = i; + if (!line.includes("):")) { + while (j + 1 < lines.length && !lines[j].includes("):")) { + j++; + sigLines.push(lines[j]); + } + } + const signature = sigLines.join("\n").trim(); + + // Collect function body (indented lines after def) + const bodyStart = j + 1; + let bodyEnd = bodyStart; + while ( + bodyEnd < lines.length && + (lines[bodyEnd].match(/^\s+/) || lines[bodyEnd].trim() === "") + ) { + bodyEnd++; + if ( + bodyEnd < lines.length && + lines[bodyEnd].match(/^(def |class |if __name__)/) + ) + break; + } + + const bodyLines = lines.slice(bodyStart, bodyEnd); + const body = bodyLines.join("\n"); + + // Extract docstring from body + const bodyText = body.trim(); + const docMatch = bodyText.match(/^"""([\s\S]*?)"""/); + const docstring = docMatch ? docMatch[1].trim() : ""; + + const fullCode = [signature, ...bodyLines].join("\n").trimEnd(); + + if (name !== "__main__" && !name.startsWith("_")) { + functions.push({ name, signature, docstring, body, fullCode }); + } + + i = bodyEnd; + } else { + i++; + } + } + + return functions; +} + +// ─── Build instruction from function context ─────────── +function buildInstruction( + fn: FunctionBlock, + moduleTitle: string, + category: string +): string { + if (fn.docstring) { + return `Write a Blender Python function to: ${fn.docstring}`; + } + + // Derive instruction from function name + const readable = fn.name + .replace(/_/g, " ") + .replace(/\b\w/g, (c) => c.toUpperCase()); + return `Write a Blender Python function: ${readable}. Category: ${category}`; +} + +// ─── Build full-script training pair ─────────────────── +function buildScriptPair( + content: string, + meta: { title: string; description: string }, + category: string, + filename: string +): TrainingPair | null { + if (!meta.title && !meta.description) return null; + + // Strip the module docstring for the output + const codeBody = content + .replace(/^"""[\s\S]*?"""/, "") + .trim(); + + if (codeBody.length < 50) return null; + + const instruction = meta.description + ? `Write a complete Blender Python script: ${meta.title}. ${meta.description}` + : `Write a complete Blender Python script: ${meta.title}`; + + return { + instruction, + output: codeBody, + category, + source_file: filename, + pair_type: "full_script", + }; +} + +// ─── Main ────────────────────────────────────────────── +function main() { + console.log("πŸš€ Generating training data from RAG scripts...\n"); + + const allFiles = findPyFiles(SCRIPTS_DIR); + console.log(`πŸ“‚ Found ${allFiles.length} Python files\n`); + + const pairs: TrainingPair[] = []; + const stats = { + files: 0, + functions: 0, + fullScripts: 0, + skipped: 0, + }; + + for (const filePath of allFiles) { + const filename = path.relative(SCRIPTS_DIR, filePath); + const content = fs.readFileSync(filePath, "utf-8"); + const category = getCategory(filePath); + const meta = parseModuleDocstring(content); + + stats.files++; + + // 1. Full-script training pair + const scriptPair = buildScriptPair(content, meta, category, filename); + if (scriptPair) { + pairs.push(scriptPair); + stats.fullScripts++; + } + + // 2. Function-level training pairs + const functions = extractFunctions(content); + for (const fn of functions) { + if (fn.fullCode.length < 30) { + stats.skipped++; + continue; + } + + const instruction = buildInstruction(fn, meta.title, category); + + pairs.push({ + instruction, + output: `import bpy\n\n${fn.fullCode}`, + category, + source_file: filename, + pair_type: "function", + }); + stats.functions++; + } + } + + // Write JSONL + const jsonlContent = pairs + .map((p) => JSON.stringify(p)) + .join("\n"); + + fs.writeFileSync(OUTPUT_FILE, jsonlContent + "\n", "utf-8"); + + // Summary + console.log("βœ… Training data generated!\n"); + console.log(` πŸ“Š Summary:`); + console.log(` Files processed: ${stats.files}`); + console.log(` Full-script pairs: ${stats.fullScripts}`); + console.log(` Function pairs: ${stats.functions}`); + console.log(` Skipped (tiny): ${stats.skipped}`); + console.log(` Total pairs: ${pairs.length}`); + console.log(`\n πŸ“ Output: ${OUTPUT_FILE}`); + + // Category breakdown + const catCounts: Record = {}; + for (const p of pairs) { + catCounts[p.category] = (catCounts[p.category] || 0) + 1; + } + console.log(`\n πŸ“‹ By category:`); + for (const [cat, count] of Object.entries(catCounts).sort( + (a, b) => b[1] - a[1] + )) { + console.log(` ${cat}: ${count}`); + } +} + +main(); diff --git a/scripts/mock-neural-server.ts b/scripts/mock-neural-server.ts new file mode 100644 index 0000000..f2d6afb --- /dev/null +++ b/scripts/mock-neural-server.ts @@ -0,0 +1,177 @@ +/** + * Mock Neural Server β€” Returns minimal valid GLB files for testing + * + * Usage: + * npx tsx scripts/mock-neural-server.ts + * npm run mock:neural + * + * Endpoints: + * GET /health β†’ 200 { status: "ok" } + * POST /generate β†’ 200 (binary GLB β€” minimal cube mesh) + */ + +import http from "http" + +const PORT = Number(process.env.MOCK_NEURAL_PORT || 8090) + +/** + * Minimal valid glTF-Binary (GLB) containing a cube mesh. + * This is a ~1.6KB binary that Blender can import successfully. + * + * Structure: + * - 12-byte GLB header (magic, version 2, total length) + * - JSON chunk with scene, mesh, accessors, bufferViews, buffer + * - Binary chunk with vertex positions (8 vertices) + indices (12 triangles) + */ +function createMinimalGLB(): Buffer { + // ── Geometry data ────────────────────────────────────────── + // Cube vertices: 8 corners, 3 floats each (x, y, z) + const positions = new Float32Array([ + -0.5, -0.5, 0.5, // 0: front-bottom-left + 0.5, -0.5, 0.5, // 1: front-bottom-right + 0.5, 0.5, 0.5, // 2: front-top-right + -0.5, 0.5, 0.5, // 3: front-top-left + -0.5, -0.5, -0.5, // 4: back-bottom-left + 0.5, -0.5, -0.5, // 5: back-bottom-right + 0.5, 0.5, -0.5, // 6: back-top-right + -0.5, 0.5, -0.5, // 7: back-top-left + ]) + + // 12 triangles (2 per face Γ— 6 faces) + const indices = new Uint16Array([ + 0, 1, 2, 0, 2, 3, // front + 1, 5, 6, 1, 6, 2, // right + 5, 4, 7, 5, 7, 6, // back + 4, 0, 3, 4, 3, 7, // left + 3, 2, 6, 3, 6, 7, // top + 4, 5, 1, 4, 1, 0, // bottom + ]) + + const positionBytes = Buffer.from(positions.buffer) + const indexBytes = Buffer.from(indices.buffer) + const binData = Buffer.concat([positionBytes, indexBytes]) + + // ── JSON scene description ───────────────────────────────── + const json = { + asset: { version: "2.0", generator: "ModelForge Mock Neural Server" }, + scene: 0, + scenes: [{ nodes: [0] }], + nodes: [{ mesh: 0, name: "NeuralMeshOutput" }], + meshes: [{ + primitives: [{ + attributes: { POSITION: 0 }, + indices: 1, + }], + }], + accessors: [ + { + bufferView: 0, + componentType: 5126, // FLOAT + count: 8, + type: "VEC3", + max: [0.5, 0.5, 0.5], + min: [-0.5, -0.5, -0.5], + }, + { + bufferView: 1, + componentType: 5123, // UNSIGNED_SHORT + count: 36, + type: "SCALAR", + }, + ], + bufferViews: [ + { buffer: 0, byteOffset: 0, byteLength: positionBytes.length, target: 34962 }, + { buffer: 0, byteOffset: positionBytes.length, byteLength: indexBytes.length, target: 34963 }, + ], + buffers: [{ byteLength: binData.length }], + } + + const jsonStr = JSON.stringify(json) + // Pad to 4-byte alignment + const jsonPadded = jsonStr + " ".repeat((4 - (jsonStr.length % 4)) % 4) + const jsonBuf = Buffer.from(jsonPadded, "utf-8") + + // Pad binary to 4-byte alignment + const binPadding = (4 - (binData.length % 4)) % 4 + const binBuf = binPadding > 0 + ? Buffer.concat([binData, Buffer.alloc(binPadding)]) + : binData + + // ── GLB assembly ─────────────────────────────────────────── + const totalLength = 12 + 8 + jsonBuf.length + 8 + binBuf.length + + const glb = Buffer.alloc(totalLength) + let offset = 0 + + // Header + glb.writeUInt32LE(0x46546C67, offset); offset += 4 // magic "glTF" + glb.writeUInt32LE(2, offset); offset += 4 // version 2 + glb.writeUInt32LE(totalLength, offset); offset += 4 // total length + + // JSON chunk + glb.writeUInt32LE(jsonBuf.length, offset); offset += 4 // chunk length + glb.writeUInt32LE(0x4E4F534A, offset); offset += 4 // chunk type "JSON" + jsonBuf.copy(glb, offset); offset += jsonBuf.length + + // Binary chunk + glb.writeUInt32LE(binBuf.length, offset); offset += 4 // chunk length + glb.writeUInt32LE(0x004E4942, offset); offset += 4 // chunk type "BIN\0" + binBuf.copy(glb, offset) + + return glb +} + +// Pre-generate the GLB so we don't rebuild on every request +const MOCK_GLB = createMinimalGLB() + +const server = http.createServer((req, res) => { + // CORS + res.setHeader("Access-Control-Allow-Origin", "*") + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + res.setHeader("Access-Control-Allow-Headers", "Content-Type") + + if (req.method === "OPTIONS") { + res.writeHead(204) + res.end() + return + } + + // Health check + if (req.url === "/health" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }) + res.end(JSON.stringify({ status: "ok", provider: "mock-neural-server" })) + return + } + + // Generate endpoint β€” returns minimal GLB + if (req.url === "/generate" && req.method === "POST") { + // Collect the request body (we don't use it, but consume it) + const chunks: Buffer[] = [] + req.on("data", (chunk: Buffer) => chunks.push(chunk)) + req.on("end", () => { + const body = Buffer.concat(chunks).toString("utf-8") + console.log(`[mock-neural] /generate request received (${body.length} bytes)`) + + // Simulate ~1s processing time + setTimeout(() => { + res.writeHead(200, { + "Content-Type": "model/gltf-binary", + "Content-Length": String(MOCK_GLB.length), + }) + res.end(MOCK_GLB) + console.log(`[mock-neural] Returned ${MOCK_GLB.length} byte GLB`) + }, 1000) + }) + return + } + + // 404 + res.writeHead(404, { "Content-Type": "application/json" }) + res.end(JSON.stringify({ error: "Not found" })) +}) + +server.listen(PORT, () => { + console.log(`\n🧠 Mock Neural Server running on http://localhost:${PORT}`) + console.log(` GET /health β†’ health check`) + console.log(` POST /generate β†’ returns minimal GLB cube mesh\n`) +}) diff --git a/training/eval_prompts.json b/training/eval_prompts.json new file mode 100644 index 0000000..2b33398 --- /dev/null +++ b/training/eval_prompts.json @@ -0,0 +1,304 @@ +{ + "prompts": [ + { + "id": 1, + "category": "materials", + "prompt": "Create a brushed aluminum material with subtle anisotropic scratches", + "difficulty": "intermediate" + }, + { + "id": 2, + "category": "materials", + "prompt": "Create a frosted glass material with IOR 1.5 and medium roughness", + "difficulty": "basic" + }, + { + "id": 3, + "category": "materials", + "prompt": "Create procedural rust material that blends orange-brown patches using voronoi noise on a dark metal base", + "difficulty": "advanced" + }, + { + "id": 4, + "category": "displacement", + "prompt": "Create a raked zen garden sand floor with wave displacement for parallel rake lines", + "difficulty": "advanced" + }, + { + "id": 5, + "category": "displacement", + "prompt": "Create a rocky terrain plane with noise-based displacement visible in the viewport", + "difficulty": "intermediate" + }, + { + "id": 6, + "category": "lighting", + "prompt": "Set up three-point lighting with key, fill, and rim lights using physical units (watts)", + "difficulty": "intermediate" + }, + { + "id": 7, + "category": "lighting", + "prompt": "Create a warm sunset lighting setup with an orange sun light at low angle", + "difficulty": "basic" + }, + { + "id": 8, + "category": "lighting", + "prompt": "Add volumetric fog to the scene with a Volume Scatter world shader", + "difficulty": "intermediate" + }, + { + "id": 9, + "category": "modeling", + "prompt": "Create a wooden treasure chest with hinged lid, iron bands, and lock", + "difficulty": "advanced" + }, + { + "id": 10, + "category": "modeling", + "prompt": "Create a low-poly tree with a cylinder trunk and icosphere canopy", + "difficulty": "basic" + }, + { + "id": 11, + "category": "modeling", + "prompt": "Create a spiral staircase with 20 steps wrapping around a central pole", + "difficulty": "advanced" + }, + { + "id": 12, + "category": "modeling", + "prompt": "Create a medieval stone wall with an arched doorway", + "difficulty": "intermediate" + }, + { + "id": 13, + "category": "scene", + "prompt": "Set up a product showcase pedestal with a circular base, backdrop curve, and area light", + "difficulty": "intermediate" + }, + { + "id": 14, + "category": "scene", + "prompt": "Create a simple outdoor scene with a ground plane, sky dome, and sun light", + "difficulty": "basic" + }, + { + "id": 15, + "category": "render", + "prompt": "Configure Cycles render settings: 256 samples, denoising on, AgX color management with Medium High Contrast look", + "difficulty": "intermediate" + }, + { + "id": 16, + "category": "render", + "prompt": "Set up EEVEE render with bloom, ambient occlusion, and screen-space reflections enabled", + "difficulty": "intermediate" + }, + { + "id": 17, + "category": "animation", + "prompt": "Create a turntable camera animation: camera orbits object at 4m distance over 120 frames", + "difficulty": "intermediate" + }, + { + "id": 18, + "category": "materials", + "prompt": "Create PBR gold material with Metallic=1.0, correct base color for gold, and low roughness", + "difficulty": "basic" + }, + { + "id": 19, + "category": "materials", + "prompt": "Create an emissive neon sign material with bright green emission color and strength 8.0", + "difficulty": "basic" + }, + { + "id": 20, + "category": "modeling", + "prompt": "Create a wine bottle with glass material and a cork stopper", + "difficulty": "intermediate" + }, + { + "id": 21, + "category": "scene", + "prompt": "Create a Japanese garden: raked sand floor, three asymmetric stones, red torii gate, warm area light, subtle mist", + "difficulty": "advanced" + }, + { + "id": 22, + "category": "displacement", + "prompt": "Create water surface with concentric ring ripple displacement", + "difficulty": "intermediate" + }, + { + "id": 23, + "category": "scene", + "prompt": "Build a solar system with sun (emissive), 4 planets at different orbits, and orbit ring indicators", + "difficulty": "advanced" + }, + { + "id": 24, + "category": "modeling", + "prompt": "Create a bookshelf with 3 shelves and 5 randomly sized books on each shelf", + "difficulty": "intermediate" + }, + { + "id": 25, + "category": "materials", + "prompt": "Create subsurface scattering skin material with warm undertone and correct Subsurface Weight", + "difficulty": "intermediate" + }, + { + "id": 26, + "category": "camera", + "prompt": "Set up a camera with 85mm focal length, f/2.8 depth of field, focused on origin", + "difficulty": "intermediate" + }, + { + "id": 27, + "category": "modeling", + "prompt": "Create a medieval castle with 4 corner towers, walls, a gate with portcullis, and battlements", + "difficulty": "advanced" + }, + { + "id": 28, + "category": "materials", + "prompt": "Create thin film iridescent soap bubble material using Thin Film Thickness and IOR", + "difficulty": "advanced" + }, + { + "id": 29, + "category": "scene", + "prompt": "Create a desk scene: table, monitor, keyboard, mouse, and desk lamp with warm light", + "difficulty": "advanced" + }, + { + "id": 30, + "category": "render", + "prompt": "Set render resolution to 1920x1080, set output format to PNG with 16-bit color depth", + "difficulty": "basic" + }, + { + "id": 31, + "category": "modeling", + "prompt": "Create a low-poly mountain range using displaced planes with noise texture", + "difficulty": "intermediate" + }, + { + "id": 32, + "category": "lighting", + "prompt": "Create dramatic rim lighting: single strong backlight with slight blue tint", + "difficulty": "basic" + }, + { + "id": 33, + "category": "materials", + "prompt": "Create worn wood material with procedural grain using wave texture on color and roughness", + "difficulty": "advanced" + }, + { + "id": 34, + "category": "scene", + "prompt": "Create an underwater scene: blue-green world, caustic light, scattered coral rocks, fish shapes", + "difficulty": "advanced" + }, + { + "id": 35, + "category": "modeling", + "prompt": "Create a chess pawn piece using a profile curve and spin/lathe technique", + "difficulty": "intermediate" + }, + { + "id": 36, + "category": "materials", + "prompt": "Create a diamond material with IOR 2.42, zero roughness, and full transmission", + "difficulty": "basic" + }, + { + "id": 37, + "category": "displacement", + "prompt": "Add wave-pattern displacement to an existing plane using Subdivision Surface + Displace modifiers", + "difficulty": "intermediate" + }, + { + "id": 38, + "category": "scene", + "prompt": "Create a forest clearing: ground plane with grass particles, 10 tree trunks, fog volume", + "difficulty": "advanced" + }, + { + "id": 39, + "category": "modeling", + "prompt": "Create a simple robot character: box body, cylinder arms and legs, sphere head with emissive eyes", + "difficulty": "intermediate" + }, + { + "id": 40, + "category": "render", + "prompt": "Switch to Cycles render engine with GPU compute device and set tile size to 256", + "difficulty": "basic" + }, + { + "id": 41, + "category": "materials", + "prompt": "Create a marble material with SSS, cool blue-white base color, and low roughness", + "difficulty": "intermediate" + }, + { + "id": 42, + "category": "scene", + "prompt": "Build a city block: 5 buildings of different heights, road plane, street lights", + "difficulty": "advanced" + }, + { + "id": 43, + "category": "animation", + "prompt": "Animate an object bouncing: keyframe Z location at frames 1, 15, 30 with ease-in-out interpolation", + "difficulty": "basic" + }, + { + "id": 44, + "category": "modeling", + "prompt": "Create a sword with tapered blade, crossguard, wrapped handle, and pommel sphere", + "difficulty": "intermediate" + }, + { + "id": 45, + "category": "materials", + "prompt": "Create car paint material: two-tone metallic with clearcoat (Coat Weight = 1.0)", + "difficulty": "advanced" + }, + { + "id": 46, + "category": "lighting", + "prompt": "Set up studio lighting: large overhead softbox area light plus two side fill lights", + "difficulty": "intermediate" + }, + { + "id": 47, + "category": "scene", + "prompt": "Create a space scene: starfield background, planet with atmosphere ring, orbiting satellite", + "difficulty": "advanced" + }, + { + "id": 48, + "category": "modeling", + "prompt": "Create a coffee mug with handle using cylinder + torus for handle attachment", + "difficulty": "intermediate" + }, + { + "id": 49, + "category": "materials", + "prompt": "Create frosted ice material combining transmission, subsurface, and high roughness", + "difficulty": "advanced" + }, + { + "id": 50, + "category": "scene", + "prompt": "Create a campfire scene: log pile, orange emissive fire planes, warm point light, ground plane", + "difficulty": "intermediate" + } + ] +} \ No newline at end of file diff --git a/training/requirements_training.txt b/training/requirements_training.txt new file mode 100644 index 0000000..6d2894d --- /dev/null +++ b/training/requirements_training.txt @@ -0,0 +1,12 @@ +# Requirements for Blender Code Generation Fine-Tuning +# Install on Azure ML A100 instance: +# pip install -r requirements_training.txt + +torch>=2.1.0 +transformers>=4.40.0 +peft>=0.10.0 +bitsandbytes>=0.43.0 +datasets>=2.18.0 +trl>=0.8.0 +accelerate>=0.28.0 +wandb diff --git a/training/train_blender_codegen.py b/training/train_blender_codegen.py new file mode 100644 index 0000000..271fc53 --- /dev/null +++ b/training/train_blender_codegen.py @@ -0,0 +1,204 @@ +""" +Blender Code Generation Fine-Tuning Script β€” QLoRA on Azure A100 +================================================================ + +Usage on Azure ML: + pip install -r requirements_training.txt + python train_blender_codegen.py --data training_data.jsonl --epochs 3 + +This script fine-tunes Qwen3-8B (or 14B) using QLoRA (4-bit NF4) +for Blender Python code generation. + +Requirements (requirements_training.txt): + torch>=2.1.0 + transformers>=4.40.0 + peft>=0.10.0 + bitsandbytes>=0.43.0 + datasets>=2.18.0 + trl>=0.8.0 + accelerate>=0.28.0 + wandb +""" + +import argparse +import json +import os + +import torch +from datasets import Dataset +from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training +from transformers import ( + AutoModelForCausalLM, + AutoTokenizer, + BitsAndBytesConfig, + TrainingArguments, +) +from trl import SFTTrainer + + +# ─── Config ───────────────────────────────────────────── +DEFAULT_MODEL = "Qwen/Qwen3-8B" +LORA_R = 64 +LORA_ALPHA = 128 +LORA_DROPOUT = 0.05 +MAX_SEQ_LEN = 2048 + +SYSTEM_PROMPT = ( + "You are a Blender Python code generator. " + "Given a natural language instruction, write correct, production-ready " + "Blender Python code using the bpy API (Blender 4.0+/5.0). " + "Follow PBR material conventions, use correct socket names, " + "and apply the factory pattern for materials and objects." +) + + +def load_training_data(path: str) -> Dataset: + """Load JSONL training data into HuggingFace Dataset.""" + records = [] + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + record = json.loads(line) + records.append(record) + + print(f"Loaded {len(records)} training pairs") + return Dataset.from_list(records) + + +def format_chat(example: dict, tokenizer) -> dict: + """Format training pair into chat template.""" + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": example["instruction"]}, + {"role": "assistant", "content": example["output"]}, + ] + text = tokenizer.apply_chat_template( + messages, tokenize=False, add_generation_prompt=False + ) + return {"text": text} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--data", required=True, help="Path to training_data.jsonl") + parser.add_argument("--model", default=DEFAULT_MODEL, help="Base model name") + parser.add_argument("--output", default="./output/blender-codegen-qlora") + parser.add_argument("--epochs", type=int, default=3) + parser.add_argument("--batch_size", type=int, default=4) + parser.add_argument("--grad_accum", type=int, default=4) + parser.add_argument("--lr", type=float, default=2e-4) + parser.add_argument("--warmup", type=float, default=0.05) + parser.add_argument("--no_wandb", action="store_true") + args = parser.parse_args() + + # ─── Quantization config (4-bit NF4) ──────────────── + bnb_config = BitsAndBytesConfig( + load_in_4bit=True, + bnb_4bit_quant_type="nf4", + bnb_4bit_compute_dtype=torch.bfloat16, + bnb_4bit_use_double_quant=True, + ) + + # ─── Load model + tokenizer ───────────────────────── + print(f"Loading model: {args.model}") + tokenizer = AutoTokenizer.from_pretrained( + args.model, trust_remote_code=True + ) + if tokenizer.pad_token is None: + tokenizer.pad_token = tokenizer.eos_token + + model = AutoModelForCausalLM.from_pretrained( + args.model, + quantization_config=bnb_config, + device_map="auto", + trust_remote_code=True, + torch_dtype=torch.bfloat16, + ) + model = prepare_model_for_kbit_training(model) + + # ─── LoRA config ──────────────────────────────────── + lora_config = LoraConfig( + r=LORA_R, + lora_alpha=LORA_ALPHA, + lora_dropout=LORA_DROPOUT, + bias="none", + task_type="CAUSAL_LM", + target_modules=[ + "q_proj", "k_proj", "v_proj", "o_proj", + "gate_proj", "up_proj", "down_proj", + ], + ) + model = get_peft_model(model, lora_config) + model.print_trainable_parameters() + + # ─── Dataset ──────────────────────────────────────── + dataset = load_training_data(args.data) + dataset = dataset.map( + lambda ex: format_chat(ex, tokenizer), + remove_columns=dataset.column_names, + ) + + # Split: 90% train, 10% eval + split = dataset.train_test_split(test_size=0.1, seed=42) + train_ds = split["train"] + eval_ds = split["test"] + print(f"Train: {len(train_ds)}, Eval: {len(eval_ds)}") + + # ─── Training args ────────────────────────────────── + training_args = TrainingArguments( + output_dir=args.output, + num_train_epochs=args.epochs, + per_device_train_batch_size=args.batch_size, + per_device_eval_batch_size=args.batch_size, + gradient_accumulation_steps=args.grad_accum, + learning_rate=args.lr, + warmup_ratio=args.warmup, + lr_scheduler_type="cosine", + bf16=True, + logging_steps=10, + eval_strategy="steps", + eval_steps=50, + save_strategy="steps", + save_steps=100, + save_total_limit=3, + load_best_model_at_end=True, + metric_for_best_model="eval_loss", + report_to="wandb" if not args.no_wandb else "none", + run_name="blender-codegen-qlora", + optim="paged_adamw_8bit", + gradient_checkpointing=True, + max_grad_norm=0.3, + group_by_length=True, + ) + + # ─── Trainer ──────────────────────────────────────── + trainer = SFTTrainer( + model=model, + train_dataset=train_ds, + eval_dataset=eval_ds, + tokenizer=tokenizer, + args=training_args, + max_seq_length=MAX_SEQ_LEN, + ) + + print("Starting training...") + trainer.train() + + # ─── Save ─────────────────────────────────────────── + final_path = os.path.join(args.output, "final") + trainer.save_model(final_path) + tokenizer.save_pretrained(final_path) + print(f"Model saved to {final_path}") + + # Merge LoRA weights for deployment + merged_path = os.path.join(args.output, "merged") + merged_model = model.merge_and_unload() + merged_model.save_pretrained(merged_path) + tokenizer.save_pretrained(merged_path) + print(f"Merged model saved to {merged_path}") + + +if __name__ == "__main__": + main() diff --git a/training/training_data.jsonl b/training/training_data.jsonl new file mode 100644 index 0000000..a994618 --- /dev/null +++ b/training/training_data.jsonl @@ -0,0 +1,284 @@ +{"instruction":"Write a complete Blender Python script: Action/Animation Utilities. Functions for managing animation actions and NLA.","output":"import bpy\r\n\r\n\r\ndef create_action(name: str) -> bpy.types.Action:\r\n \"\"\"Create a new action.\"\"\"\r\n return bpy.data.actions.new(name)\r\n\r\n\r\ndef assign_action(\r\n obj: bpy.types.Object,\r\n action: bpy.types.Action\r\n) -> None:\r\n \"\"\"Assign action to object.\"\"\"\r\n if not obj.animation_data:\r\n obj.animation_data_create()\r\n obj.animation_data.action = action\r\n\r\n\r\ndef duplicate_action(\r\n action: bpy.types.Action,\r\n new_name: str = None\r\n) -> bpy.types.Action:\r\n \"\"\"Duplicate an action.\"\"\"\r\n new_action = action.copy()\r\n if new_name:\r\n new_action.name = new_name\r\n return new_action\r\n\r\n\r\ndef push_action_to_nla(\r\n obj: bpy.types.Object,\r\n action: bpy.types.Action = None,\r\n track_name: str = \"NLATrack\"\r\n) -> bpy.types.NlaStrip:\r\n \"\"\"Push action to NLA track.\"\"\"\r\n if not obj.animation_data:\r\n obj.animation_data_create()\r\n \r\n action = action or obj.animation_data.action\r\n if not action:\r\n return None\r\n \r\n track = obj.animation_data.nla_tracks.new()\r\n track.name = track_name\r\n \r\n strip = track.strips.new(action.name, int(action.frame_range[0]), action)\r\n \r\n return strip\r\n\r\n\r\ndef create_nla_strip(\r\n obj: bpy.types.Object,\r\n action: bpy.types.Action,\r\n start_frame: int,\r\n track_name: str = \"Track\"\r\n) -> bpy.types.NlaStrip:\r\n \"\"\"Create NLA strip from action.\"\"\"\r\n if not obj.animation_data:\r\n obj.animation_data_create()\r\n \r\n track = None\r\n for t in obj.animation_data.nla_tracks:\r\n if t.name == track_name:\r\n track = t\r\n break\r\n \r\n if not track:\r\n track = obj.animation_data.nla_tracks.new()\r\n track.name = track_name\r\n \r\n strip = track.strips.new(action.name, start_frame, action)\r\n return strip\r\n\r\n\r\ndef set_strip_blend_mode(\r\n strip: bpy.types.NlaStrip,\r\n blend_type: str = 'REPLACE'\r\n) -> None:\r\n \"\"\"Set NLA strip blend mode.\"\"\"\r\n strip.blend_type = blend_type # REPLACE, ADD, SUBTRACT, MULTIPLY\r\n\r\n\r\ndef set_strip_extrapolation(\r\n strip: bpy.types.NlaStrip,\r\n extrapolation: str = 'HOLD'\r\n) -> None:\r\n \"\"\"Set NLA strip extrapolation.\"\"\"\r\n strip.extrapolation = extrapolation # NOTHING, HOLD, HOLD_FORWARD\r\n\r\n\r\ndef scale_action(\r\n action: bpy.types.Action,\r\n scale: float\r\n) -> None:\r\n \"\"\"Scale action keyframes.\"\"\"\r\n for fcurve in action.fcurves:\r\n for keyframe in fcurve.keyframe_points:\r\n keyframe.co.x *= scale\r\n keyframe.handle_left.x *= scale\r\n keyframe.handle_right.x *= scale\r\n\r\n\r\ndef offset_action(\r\n action: bpy.types.Action,\r\n offset: float\r\n) -> None:\r\n \"\"\"Offset action keyframes.\"\"\"\r\n for fcurve in action.fcurves:\r\n for keyframe in fcurve.keyframe_points:\r\n keyframe.co.x += offset\r\n keyframe.handle_left.x += offset\r\n keyframe.handle_right.x += offset\r\n\r\n\r\ndef list_actions() -> list:\r\n \"\"\"Return list of all action names.\"\"\"\r\n return [a.name for a in bpy.data.actions]\r\n\r\n\r\ndef delete_action(name: str) -> None:\r\n \"\"\"Delete action by name.\"\"\"\r\n if name in bpy.data.actions:\r\n bpy.data.actions.remove(bpy.data.actions[name])\r\n\r\n\r\ndef clear_animation(obj: bpy.types.Object) -> None:\r\n \"\"\"Clear all animation from object.\"\"\"\r\n if obj.animation_data:\r\n obj.animation_data.action = None\r\n for track in obj.animation_data.nla_tracks:\r\n obj.animation_data.nla_tracks.remove(track)","category":"utility","source_file":"action_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Create Action. Category: utility","output":"import bpy\n\ndef create_action(name: str) -> bpy.types.Action:\r\n \"\"\"Create a new action.\"\"\"\r\n return bpy.data.actions.new(name)\r\n\r\n\r\ndef assign_action(\r\n obj: bpy.types.Object,\r\n action: bpy.types.Action\r\n) -> None:\r\n \"\"\"Assign action to object.\"\"\"\r\n if not obj.animation_data:\r\n obj.animation_data_create()\r\n obj.animation_data.action = action\r\n\r\n\r\ndef duplicate_action(\r\n action: bpy.types.Action,\r\n new_name: str = None\r\n) -> bpy.types.Action:\r\n \"\"\"Duplicate an action.\"\"\"\r\n new_action = action.copy()\r\n if new_name:\r\n new_action.name = new_name\r\n return new_action\r\n\r\n\r\ndef push_action_to_nla(\r\n obj: bpy.types.Object,\r\n action: bpy.types.Action = None,\r\n track_name: str = \"NLATrack\"\r\n) -> bpy.types.NlaStrip:\r\n \"\"\"Push action to NLA track.\"\"\"\r\n if not obj.animation_data:\r\n obj.animation_data_create()\r\n \r\n action = action or obj.animation_data.action\r\n if not action:\r\n return None\r\n \r\n track = obj.animation_data.nla_tracks.new()\r\n track.name = track_name\r\n \r\n strip = track.strips.new(action.name, int(action.frame_range[0]), action)\r\n \r\n return strip\r\n\r\n\r\ndef create_nla_strip(\r\n obj: bpy.types.Object,\r\n action: bpy.types.Action,\r\n start_frame: int,\r\n track_name: str = \"Track\"\r\n) -> bpy.types.NlaStrip:\r\n \"\"\"Create NLA strip from action.\"\"\"\r\n if not obj.animation_data:\r\n obj.animation_data_create()\r\n \r\n track = None\r\n for t in obj.animation_data.nla_tracks:\r\n if t.name == track_name:\r\n track = t\r\n break\r\n \r\n if not track:\r\n track = obj.animation_data.nla_tracks.new()\r\n track.name = track_name\r\n \r\n strip = track.strips.new(action.name, start_frame, action)\r\n return strip\r\n\r\n\r\ndef set_strip_blend_mode(\r\n strip: bpy.types.NlaStrip,\r\n blend_type: str = 'REPLACE'\r\n) -> None:\r\n \"\"\"Set NLA strip blend mode.\"\"\"\r\n strip.blend_type = blend_type # REPLACE, ADD, SUBTRACT, MULTIPLY\r\n\r\n\r\ndef set_strip_extrapolation(\r\n strip: bpy.types.NlaStrip,\r\n extrapolation: str = 'HOLD'\r\n) -> None:\r\n \"\"\"Set NLA strip extrapolation.\"\"\"\r\n strip.extrapolation = extrapolation # NOTHING, HOLD, HOLD_FORWARD\r\n\r\n\r\ndef scale_action(\r\n action: bpy.types.Action,\r\n scale: float\r\n) -> None:\r\n \"\"\"Scale action keyframes.\"\"\"\r\n for fcurve in action.fcurves:\r\n for keyframe in fcurve.keyframe_points:\r\n keyframe.co.x *= scale\r\n keyframe.handle_left.x *= scale\r\n keyframe.handle_right.x *= scale\r\n\r\n\r\ndef offset_action(\r\n action: bpy.types.Action,\r\n offset: float\r\n) -> None:\r\n \"\"\"Offset action keyframes.\"\"\"\r\n for fcurve in action.fcurves:\r\n for keyframe in fcurve.keyframe_points:\r\n keyframe.co.x += offset\r\n keyframe.handle_left.x += offset\r\n keyframe.handle_right.x += offset\r\n\r\n\r\ndef list_actions() -> list:\r\n \"\"\"Return list of all action names.\"\"\"\r\n return [a.name for a in bpy.data.actions]\r\n\r\n\r\ndef delete_action(name: str) -> None:\r\n \"\"\"Delete action by name.\"\"\"\r\n if name in bpy.data.actions:\r\n bpy.data.actions.remove(bpy.data.actions[name])\r\n\r\n\r\ndef clear_animation(obj: bpy.types.Object) -> None:\r\n \"\"\"Clear all animation from object.\"\"\"\r\n if obj.animation_data:\r\n obj.animation_data.action = None\r\n for track in obj.animation_data.nla_tracks:\r\n obj.animation_data.nla_tracks.remove(track)","category":"utility","source_file":"action_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Animation Utilities. Functions for keyframing, timeline control, and animation setup in Blender.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef set_keyframe(\r\n obj: bpy.types.Object,\r\n data_path: str,\r\n frame: int,\r\n value = None,\r\n index: int = -1\r\n) -> None:\r\n \"\"\"\r\n Insert a keyframe for an object property.\r\n \r\n Args:\r\n obj: Object to keyframe\r\n data_path: Property path (e.g., 'location', 'rotation_euler', 'scale')\r\n frame: Frame number\r\n value: Value to set (optional, uses current if None)\r\n index: Array index (-1 for all)\r\n \r\n Example:\r\n >>> set_keyframe(cube, 'location', 1)\r\n >>> cube.location.z = 5\r\n >>> set_keyframe(cube, 'location', 60)\r\n \"\"\"\r\n if value is not None:\r\n if index >= 0:\r\n getattr(obj, data_path)[index] = value\r\n else:\r\n setattr(obj, data_path, value)\r\n \r\n obj.keyframe_insert(data_path=data_path, frame=frame, index=index)\r\n\r\n\r\ndef animate_property(\r\n obj: bpy.types.Object,\r\n data_path: str,\r\n keyframes: list,\r\n index: int = -1\r\n) -> None:\r\n \"\"\"\r\n Animate a property with multiple keyframes.\r\n \r\n Args:\r\n obj: Object to animate\r\n data_path: Property path\r\n keyframes: List of (frame, value) tuples\r\n index: Array index (-1 for all)\r\n \r\n Example:\r\n >>> animate_property(cube, 'location', [(1, (0,0,0)), (30, (0,0,5)), (60, (0,0,0))])\r\n \"\"\"\r\n for frame, value in keyframes:\r\n set_keyframe(obj, data_path, frame, value, index)\r\n\r\n\r\ndef set_interpolation(\r\n obj: bpy.types.Object,\r\n interpolation: str = 'BEZIER'\r\n) -> None:\r\n \"\"\"\r\n Set interpolation mode for all keyframes of an object.\r\n \r\n Args:\r\n obj: Animated object\r\n interpolation: 'CONSTANT', 'LINEAR', 'BEZIER', 'SINE', 'QUAD', 'CUBIC', etc.\r\n \r\n Example:\r\n >>> set_interpolation(cube, 'LINEAR')\r\n \"\"\"\r\n if obj.animation_data and obj.animation_data.action:\r\n for fcurve in obj.animation_data.action.fcurves:\r\n for keyframe in fcurve.keyframe_points:\r\n keyframe.interpolation = interpolation\r\n\r\n\r\ndef set_frame_range(start: int, end: int, current: int = None) -> None:\r\n \"\"\"\r\n Set the animation frame range.\r\n \r\n Args:\r\n start: Start frame\r\n end: End frame\r\n current: Current frame (optional)\r\n \r\n Example:\r\n >>> set_frame_range(1, 250, 1)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.frame_start = start\r\n scene.frame_end = end\r\n if current is not None:\r\n scene.frame_set(current)\r\n\r\n\r\ndef set_fps(fps: int = 24) -> None:\r\n \"\"\"\r\n Set the scene frame rate.\r\n \r\n Args:\r\n fps: Frames per second\r\n \r\n Example:\r\n >>> set_fps(30) # Standard video\r\n >>> set_fps(24) # Film\r\n \"\"\"\r\n bpy.context.scene.render.fps = fps\r\n bpy.context.scene.render.fps_base = 1.0\r\n\r\n\r\ndef create_bounce_animation(\r\n obj: bpy.types.Object,\r\n height: float = 2.0,\r\n duration: int = 30,\r\n start_frame: int = 1,\r\n bounces: int = 3\r\n) -> None:\r\n \"\"\"\r\n Create a bouncing animation with decreasing height.\r\n \r\n Args:\r\n obj: Object to animate\r\n height: Initial bounce height\r\n duration: Total frames\r\n start_frame: Starting frame\r\n bounces: Number of bounces\r\n \r\n Example:\r\n >>> create_bounce_animation(ball, height=3, duration=60, bounces=4)\r\n \"\"\"\r\n base_z = obj.location.z\r\n frames_per_bounce = duration // bounces\r\n \r\n frame = start_frame\r\n for i in range(bounces):\r\n bounce_height = height * (0.6 ** i)\r\n \r\n # Ground position\r\n obj.location.z = base_z\r\n obj.keyframe_insert('location', frame=frame, index=2)\r\n \r\n # Peak position\r\n peak_frame = frame + frames_per_bounce // 2\r\n obj.location.z = base_z + bounce_height\r\n obj.keyframe_insert('location', frame=peak_frame, index=2)\r\n \r\n frame += frames_per_bounce\r\n \r\n # Final ground position\r\n obj.location.z = base_z\r\n obj.keyframe_insert('location', frame=frame, index=2)\r\n\r\n\r\ndef create_rotation_animation(\r\n obj: bpy.types.Object,\r\n axis: str = 'Z',\r\n degrees: float = 360,\r\n duration: int = 60,\r\n start_frame: int = 1,\r\n cycles: int = 1\r\n) -> None:\r\n \"\"\"\r\n Create a rotation animation (e.g., for turntables).\r\n \r\n Args:\r\n obj: Object to rotate\r\n axis: 'X', 'Y', or 'Z'\r\n degrees: Total rotation per cycle\r\n duration: Frames per cycle\r\n start_frame: Starting frame\r\n cycles: Number of rotations\r\n \r\n Example:\r\n >>> create_rotation_animation(product, axis='Z', degrees=360, duration=120)\r\n \"\"\"\r\n axis_index = {'X': 0, 'Y': 1, 'Z': 2}[axis.upper()]\r\n \r\n for cycle in range(cycles):\r\n frame_start = start_frame + (cycle * duration)\r\n frame_end = frame_start + duration\r\n \r\n # Start rotation\r\n obj.keyframe_insert('rotation_euler', frame=frame_start, index=axis_index)\r\n \r\n # End rotation\r\n obj.rotation_euler[axis_index] += math.radians(degrees)\r\n obj.keyframe_insert('rotation_euler', frame=frame_end, index=axis_index)\r\n \r\n # Set linear interpolation for smooth rotation\r\n set_interpolation(obj, 'LINEAR')\r\n\r\n\r\ndef create_path_animation(\r\n obj: bpy.types.Object,\r\n path: bpy.types.Object,\r\n duration: int = 100,\r\n follow_curve: bool = True\r\n) -> bpy.types.Constraint:\r\n \"\"\"\r\n Animate an object along a curve path.\r\n \r\n Args:\r\n obj: Object to animate\r\n path: Curve object to follow\r\n duration: Animation duration in frames\r\n follow_curve: Rotate object to follow path direction\r\n \r\n Returns:\r\n The Follow Path constraint\r\n \r\n Example:\r\n >>> constraint = create_path_animation(car, road_curve, duration=200)\r\n \"\"\"\r\n constraint = obj.constraints.new('FOLLOW_PATH')\r\n constraint.target = path\r\n constraint.use_curve_follow = follow_curve\r\n \r\n # Animate the offset\r\n constraint.offset = 0\r\n constraint.keyframe_insert('offset', frame=1)\r\n constraint.offset = -100\r\n constraint.keyframe_insert('offset', frame=duration)\r\n \r\n return constraint","category":"utility","source_file":"animation_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Set Frame Range. Category: utility","output":"import bpy\n\ndef set_frame_range(start: int, end: int, current: int = None) -> None:\r\n \"\"\"\r\n Set the animation frame range.\r\n \r\n Args:\r\n start: Start frame\r\n end: End frame\r\n current: Current frame (optional)\r\n \r\n Example:\r\n >>> set_frame_range(1, 250, 1)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.frame_start = start\r\n scene.frame_end = end\r\n if current is not None:\r\n scene.frame_set(current)\r\n\r\n\r\ndef set_fps(fps: int = 24) -> None:\r\n \"\"\"\r\n Set the scene frame rate.\r\n \r\n Args:\r\n fps: Frames per second\r\n \r\n Example:\r\n >>> set_fps(30) # Standard video\r\n >>> set_fps(24) # Film\r\n \"\"\"\r\n bpy.context.scene.render.fps = fps\r\n bpy.context.scene.render.fps_base = 1.0\r\n\r\n\r\ndef create_bounce_animation(\r\n obj: bpy.types.Object,\r\n height: float = 2.0,\r\n duration: int = 30,\r\n start_frame: int = 1,\r\n bounces: int = 3\r\n) -> None:\r\n \"\"\"\r\n Create a bouncing animation with decreasing height.\r\n \r\n Args:\r\n obj: Object to animate\r\n height: Initial bounce height\r\n duration: Total frames\r\n start_frame: Starting frame\r\n bounces: Number of bounces\r\n \r\n Example:\r\n >>> create_bounce_animation(ball, height=3, duration=60, bounces=4)\r\n \"\"\"\r\n base_z = obj.location.z\r\n frames_per_bounce = duration // bounces\r\n \r\n frame = start_frame\r\n for i in range(bounces):\n bounce_height = height * (0.6 ** i)\r\n \r\n # Ground position\r\n obj.location.z = base_z\r\n obj.keyframe_insert('location', frame=frame, index=2)\r\n \r\n # Peak position\r\n peak_frame = frame + frames_per_bounce // 2\r\n obj.location.z = base_z + bounce_height\r\n obj.keyframe_insert('location', frame=peak_frame, index=2)\r\n \r\n frame += frames_per_bounce\r\n \r\n # Final ground position\r\n obj.location.z = base_z\r\n obj.keyframe_insert('location', frame=frame, index=2)","category":"utility","source_file":"animation_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Blender Python API Version Compatibility Guide. Critical API compatibility reference for Blender 4.0 and 5.0. Documents all breaking changes, renamed APIs, deprecated functions, and their modern replacements. Essential knowledge for generating code that works with current Blender versions. Sourced from official Blender developer release notes.","output":"import bpy\r\n\r\n# =============================================================================\r\n# BLENDER PYTHON API β€” VERSION COMPATIBILITY GUIDE\r\n# =============================================================================\r\n#\r\n# This file documents critical breaking changes between Blender versions.\r\n# Code generated for ModelForge MUST follow these rules to avoid errors.\r\n#\r\n# Sources:\r\n# - https://developer.blender.org/docs/release_notes/5.0/python_api/\r\n# - https://developer.blender.org/docs/release_notes/4.0/python_api/\r\n# - https://docs.blender.org/api/current/info_best_practice.html\r\n# =============================================================================\r\n\r\n\r\n# =============================================================================\r\n# BLENDER 5.0 BREAKING CHANGES (CRITICAL)\r\n# =============================================================================\r\n\r\n# --- 1. material.use_nodes is DEPRECATED ---\r\n# In Blender 5.0+, materials ALWAYS have node trees.\r\n# `material.use_nodes = True` has NO EFFECT and will be removed in 6.0.\r\n#\r\n# OLD (pre-5.0):\r\n# mat = bpy.data.materials.new(\"MyMat\")\r\n# mat.use_nodes = True # Was required to create node tree\r\n#\r\n# NEW (5.0+):\r\n# mat = bpy.data.materials.new(\"MyMat\") # Node tree created automatically\r\n# # mat.use_nodes = True # DEPRECATED β€” has no effect\r\n#\r\n# SAFE for BOTH versions (works 4.x and 5.0):\r\n# mat = bpy.data.materials.new(\"MyMat\")\r\n# mat.use_nodes = True # Harmless in 5.0, required in 4.x\r\n\r\n\r\n# --- 2. world.use_nodes is DEPRECATED ---\r\n# Same as materials β€” worlds always have node trees in 5.0.\r\n#\r\n# SAFE:\r\n# world = bpy.data.worlds.new(\"MyWorld\")\r\n# world.use_nodes = True # Harmless in 5.0\r\n\r\n# --- 3. Boolean solver \"FAST\" renamed to \"FLOAT\" ---\r\n# The FAST boolean solver was renamed to FLOAT in Blender 5.0.\r\n# Valid solvers in 5.0: 'EXACT', 'FLOAT', 'MANIFOLD'\r\n# Invalid in 5.0: 'FAST' (will raise error)\r\n# \r\n# ALWAYS use 'EXACT' for best results:\r\n# modifier.solver = 'EXACT'\r\n# modifier.use_self = True\r\n# modifier.use_hole_tolerant = True\r\n\r\n\r\n# --- 4. EEVEE Engine Identifier Changed ---\r\n# OLD: scene.render.engine = 'BLENDER_EEVEE_NEXT'\r\n# NEW: scene.render.engine = 'BLENDER_EEVEE'\r\n#\r\n# 'BLENDER_EEVEE_NEXT' was the temporary name during EEVEE Next development.\r\n# In 5.0, it's now simply 'BLENDER_EEVEE'.\r\n\r\n\r\n# --- 5. Compositor Node Tree Access Changed ---\r\n# OLD: scene.node_tree (removed in 5.0)\r\n# NEW: scene.compositing_node_group\r\n#\r\n# OLD: scene.use_nodes = True\r\n# NEW: scene.use_nodes is deprecated (always True)\r\n\r\n\r\n# --- 6. Grease Pencil / Annotations Renamed ---\r\n# OLD: bpy.types.GreasePencil β†’ NEW: bpy.types.Annotation\r\n# OLD: bpy.data.grease_pencils β†’ NEW: bpy.types.annotations\r\n# OLD: bpy.types.GreasePencilv3 β†’ NEW: bpy.types.GreasePencil\r\n# OLD: bpy.data.grease_pencils_v3 β†’ NEW: bpy.data.grease_pencils\r\n\r\n\r\n# --- 7. Many Compositor Nodes Replaced ---\r\n# Compositor-specific nodes like CompositorNodeGamma are replaced by\r\n# their ShaderNode equivalents:\r\n# OLD: nodes.new(\"CompositorNodeGamma\")\r\n# NEW: nodes.new(\"ShaderNodeGamma\")\r\n\r\n\r\n# --- 8. mathutils Vector now float32 ---\r\n# In 5.0, mathutils types use float32 (was float64).\r\n# This shouldn't affect most code, but be aware of precision differences.\r\n\r\n\r\n# --- 9. Render Passes Renamed (5.0) ---\r\n# OLD: 'DiffCol' β†’ NEW: 'Diffuse Color'\r\n# OLD: 'IndexMA' β†’ NEW: 'Material Index'\r\n# OLD: 'Z' β†’ NEW: 'Depth'\r\n# render.render() now accepts frame_start and frame_end arguments.\r\n\r\n\r\n# --- 10. BGL Module REMOVED (5.0) ---\r\n# The deprecated bgl module is FULLY REMOVED in 5.0.\r\n# Image.bindcode is removed. Use gpu.texture.from_image(image) instead.\r\n# Creating shaders from GLSL source strings is removed.\r\n# Use the gpu module for all GPU operations.\r\n\r\n\r\n# --- 11. Action / Animation API Changes (5.0) ---\r\n# action.fcurves, action.groups, action.id_root β†’ REMOVED\r\n# Access FCurves via Channelbags: channelbag.fcurves\r\n# Bone.hide now only affects edit bone visibility.\r\n# For Object/Pose mode, use PoseBone.hide\r\n# Bone.select, select_head, select_tail β†’ REMOVED\r\n# Use EditBone properties or PoseBone.select\r\n# INSERTKEY_XYZ_TO_RGB flag fully removed.\r\n\r\n\r\n# --- 12. UV Selection Changes (5.0) ---\r\n# UV selection is now shared between UV maps.\r\n# REMOVED: MeshUVLoopLayer.vertex_selection, edge_selection\r\n# REMOVED: bmesh.types.BMLoopUV.select, select_edge\r\n# ADDED: uv_select_vert, uv_select_edge, uv_select_face attributes\r\n# UV pin property no longer auto-creates attribute β€” use _ensure() functions.\r\n\r\n\r\n# --- 13. Paint & Sculpt Property Renames (5.0) ---\r\n# brush.sculpt_tool β†’ brush.sculpt_brush_type\r\n# curve_preset β†’ curve_distance_falloff_preset\r\n# unified_paint_settings moved from tool settings to mode-specific Paint structs.\r\n# Radial symmetry moved from scene tool settings to mesh.radial_symmetry\r\n# REMOVED: brush.use_custom_icon, brush.icon_filepath\r\n\r\n\r\n# --- 14. File Output Node Changes (5.0) ---\r\n# Compositor File Output node:\r\n# REMOVED: file_slots, layer_slots, base_path\r\n# ADDED: directory, file_name, file_output_items\r\n\r\n\r\n# --- 15. Import/Export Changes (5.0) ---\r\n# Alembic: Scene.alembic_export REMOVED β†’ use bpy.ops.wm.alembic_export\r\n# USD Import: import_subdiv β†’ import_subdivision\r\n# attr_import_mode β†’ property_import_mode\r\n# USD Export: export_textures REMOVED (use export_textures_mode)\r\n# visible_objects_only REMOVED\r\n\r\n\r\n# --- 16. Dictionary Property Access REMOVED (5.0) ---\r\n# Properties defined via bpy.props are NO LONGER accessible via dict syntax.\r\n# OLD: bpy.context.scene['cycles'] β†’ FAILS in 5.0\r\n# NEW: bpy.context.scene.cycles β†’ Use attribute access\r\n# del obj['prop'] β†’ obj.property_unset('prop')\r\n\r\n\r\n# --- 17. Sky Texture Changes (5.0) ---\r\n# REMOVED inputs: sun_direction, turbidity, ground_albedo\r\n\r\n\r\n# --- 18. UI/Theme Changes (5.0) ---\r\n# RADIAL_MENU layout type β†’ renamed to PIE_MENU\r\n# RNA_ADD icon REMOVED\r\n# Removed theme properties: navigation_bar, execution_buts, tab_active, etc.\r\n\r\n# =============================================================================\r\n# BLENDER 4.0 BREAKING CHANGES\r\n# =============================================================================\r\n\r\n# --- Principled BSDF Changes (4.0) ---\r\n# CRITICAL: Input names changed on the Principled BSDF node.\r\n#\r\n# REMOVED inputs (4.0):\r\n# - \"Subsurface\" β†’ replaced by \"Subsurface Weight\"\r\n# - \"Subsurface Color\" β†’ replaced by \"Subsurface Radius\"\r\n# - \"Specular\" β†’ replaced by \"Specular IOR Level\"\r\n# - \"Clearcoat\" β†’ replaced by \"Coat Weight\"\r\n# - \"Clearcoat Roughness\" β†’ replaced by \"Coat Roughness\"\r\n# - \"Sheen\" β†’ replaced by \"Sheen Weight\"\r\n# - \"Sheen Tint\" β†’ replaced by \"Sheen Roughness\"\r\n# - \"Transmission\" β†’ replaced by \"Transmission Weight\"\r\n# - \"IOR\" β†’ replaced by \"IOR\" (kept, no change)\r\n# - \"Emission\" β†’ split into \"Emission Color\" + \"Emission Strength\"\r\n#\r\n# SAFE CODE for Principled BSDF (4.0+):\r\n# bsdf.inputs['Base Color'].default_value = (r, g, b, 1.0)\r\n# bsdf.inputs['Metallic'].default_value = 0.0\r\n# bsdf.inputs['Roughness'].default_value = 0.5\r\n# bsdf.inputs['Emission Color'].default_value = (r, g, b, 1.0)\r\n# bsdf.inputs['Emission Strength'].default_value = 5.0\r\n# bsdf.inputs['Coat Weight'].default_value = 0.0\r\n# bsdf.inputs['Sheen Weight'].default_value = 0.0\r\n# bsdf.inputs['Transmission Weight'].default_value = 0.0\r\n\r\n\r\n# --- Color Ramp / Mix Node Changes (4.0) ---\r\n# OLD: ShaderNodeMixRGB β†’ REMOVED in 4.0\r\n# NEW: ShaderNodeMix β†’ Use this instead\r\n#\r\n# When using ShaderNodeMix for colors:\r\n# mix_node = nodes.new('ShaderNodeMix')\r\n# mix_node.data_type = 'RGBA'\r\n# mix_node.inputs[6].default_value = (r1, g1, b1, 1) # Color A\r\n# mix_node.inputs[7].default_value = (r2, g2, b2, 1) # Color B\r\n# # Output: mix_node.outputs[2] (Result Color)\r\n\r\n\r\n# =============================================================================\r\n# SAFE PATTERNS β€” ALWAYS USE THESE\r\n# =============================================================================\r\n\r\ndef create_material_safe(name, color=(0.8, 0.8, 0.8)):\r\n \"\"\"\r\n Create a material that works across Blender 4.x and 5.x.\r\n \r\n Uses only current, non-deprecated API calls.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n # use_nodes is deprecated in 5.0 but harmless; required in 4.x\r\n mat.use_nodes = True\r\n \r\n nodes = mat.node_tree.nodes\r\n # The default node tree already has a Principled BSDF\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n if not bsdf:\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n \r\n # Use 4.0+ input names\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n \r\n return mat, bsdf\r\n\r\n\r\ndef set_emission_safe(bsdf, color=(1, 1, 1), strength=5.0):\r\n \"\"\"\r\n Set emission on a Principled BSDF using 4.0+ API.\r\n \r\n IMPORTANT: Always set BOTH Emission Color AND Emission Strength.\r\n In Blender 4.0+, these are separate inputs (was single 'Emission' before).\r\n \"\"\"\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = strength\r\n\r\n\r\ndef set_glass_safe(bsdf, color=(1, 1, 1), ior=1.45, roughness=0.0):\r\n \"\"\"\r\n Configure glass material using 4.0+ API.\r\n \r\n Uses 'Transmission Weight' (not old 'Transmission').\r\n \"\"\"\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Transmission Weight'].default_value = 1.0\r\n bsdf.inputs['IOR'].default_value = ior\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n\r\n\r\ndef set_coat_safe(bsdf, weight=1.0, roughness=0.1):\r\n \"\"\"\r\n Add clearcoat using 4.0+ API.\r\n \r\n Uses 'Coat Weight' and 'Coat Roughness' (not old 'Clearcoat').\r\n \"\"\"\r\n bsdf.inputs['Coat Weight'].default_value = weight\r\n bsdf.inputs['Coat Roughness'].default_value = roughness\r\n\r\n\r\n# =============================================================================\r\n# STYLE CONVENTIONS (Official Blender Python Guidelines)\r\n# =============================================================================\r\n#\r\n# - Use single quotes for enums: file_format = 'PNG'\r\n# - Use double quotes for strings: filepath = \"//render_out\"\r\n# - Follow PEP 8 naming: CamelCase for classes, snake_case for functions\r\n# - Explicit imports only β€” never use `from bpy import *`\r\n# - Prefer `float()` over `eval()` for parsing numbers\r\n# - Use `startswith()` / `endswith()` instead of string slicing\r\n# - Prefer list comprehensions over manual loops\r\n# - Use `if` over `try/except` in hot loops (try is slower)\r\n# - Time your scripts with `time.time()` for performance awareness\r\n# =============================================================================","category":"utility","source_file":"api_version_compatibility.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Create a material that works across Blender 4.x and 5.x.\r\n \r\n Uses only current, non-deprecated API calls.","output":"import bpy\n\ndef create_material_safe(name, color=(0.8, 0.8, 0.8)):\n \"\"\"\r\n Create a material that works across Blender 4.x and 5.x.\r\n \r\n Uses only current, non-deprecated API calls.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n # use_nodes is deprecated in 5.0 but harmless; required in 4.x\r\n mat.use_nodes = True\r\n \r\n nodes = mat.node_tree.nodes\r\n # The default node tree already has a Principled BSDF\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n if not bsdf:\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n \r\n # Use 4.0+ input names\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n \r\n return mat, bsdf","category":"utility","source_file":"api_version_compatibility.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Set emission on a Principled BSDF using 4.0+ API.\r\n \r\n IMPORTANT: Always set BOTH Emission Color AND Emission Strength.\r\n In Blender 4.0+, these are separate inputs (was single 'Emission' before).","output":"import bpy\n\ndef set_emission_safe(bsdf, color=(1, 1, 1), strength=5.0):\n \"\"\"\r\n Set emission on a Principled BSDF using 4.0+ API.\r\n \r\n IMPORTANT: Always set BOTH Emission Color AND Emission Strength.\r\n In Blender 4.0+, these are separate inputs (was single 'Emission' before).\r\n \"\"\"\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = strength","category":"utility","source_file":"api_version_compatibility.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Configure glass material using 4.0+ API.\r\n \r\n Uses 'Transmission Weight' (not old 'Transmission').","output":"import bpy\n\ndef set_glass_safe(bsdf, color=(1, 1, 1), ior=1.45, roughness=0.0):\n \"\"\"\r\n Configure glass material using 4.0+ API.\r\n \r\n Uses 'Transmission Weight' (not old 'Transmission').\r\n \"\"\"\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Transmission Weight'].default_value = 1.0\r\n bsdf.inputs['IOR'].default_value = ior\r\n bsdf.inputs['Roughness'].default_value = roughness","category":"utility","source_file":"api_version_compatibility.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Add clearcoat using 4.0+ API.\r\n \r\n Uses 'Coat Weight' and 'Coat Roughness' (not old 'Clearcoat').","output":"import bpy\n\ndef set_coat_safe(bsdf, weight=1.0, roughness=0.1):\n \"\"\"\r\n Add clearcoat using 4.0+ API.\r\n \r\n Uses 'Coat Weight' and 'Coat Roughness' (not old 'Clearcoat').\r\n \"\"\"\r\n bsdf.inputs['Coat Weight'].default_value = weight\r\n bsdf.inputs['Coat Roughness'].default_value = roughness","category":"utility","source_file":"api_version_compatibility.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Armature and Rigging Utilities. Functions for creating and manipulating armatures, bones, and rigging setups.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_bone(\r\n armature: bpy.types.Object,\r\n name: str,\r\n head: tuple,\r\n tail: tuple,\r\n parent_bone: str = None,\r\n connect: bool = False\r\n) -> bpy.types.EditBone:\r\n \"\"\"\r\n Add a bone to an armature.\r\n \r\n Args:\r\n armature: Armature object\r\n name: Bone name\r\n head: Head position (ball joint)\r\n tail: Tail position (direction)\r\n parent_bone: Name of parent bone\r\n connect: Connect to parent (share same position)\r\n \r\n Returns:\r\n The created bone (in edit mode)\r\n \r\n Example:\r\n >>> bone = create_bone(arm, \"Spine\", (0,0,1), (0,0,1.5))\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n bone = armature.data.edit_bones.new(name)\r\n bone.head = head\r\n bone.tail = tail\r\n \r\n if parent_bone:\r\n parent = armature.data.edit_bones.get(parent_bone)\r\n if parent:\r\n bone.parent = parent\r\n bone.use_connect = connect\r\n \r\n return bone\r\n\r\n\r\ndef create_bone_chain(\r\n armature: bpy.types.Object,\r\n positions: list,\r\n base_name: str = \"Bone\",\r\n parent_bone: str = None\r\n) -> list:\r\n \"\"\"\r\n Create a chain of connected bones.\r\n \r\n Args:\r\n armature: Armature object\r\n positions: List of XYZ tuples defining joint positions\r\n base_name: Base name for bones (numbered automatically)\r\n parent_bone: Name of bone to parent first bone to\r\n \r\n Returns:\r\n List of created bone names\r\n \r\n Example:\r\n >>> positions = [(0,0,0), (0,0,1), (0,0,2), (0,0,3)]\r\n >>> bones = create_bone_chain(arm, positions, \"Spine\")\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n bone_names = []\r\n prev_bone = parent_bone\r\n \r\n for i in range(len(positions) - 1):\r\n name = f\"{base_name}.{i:03d}\" if len(positions) > 2 else base_name\r\n \r\n bone = armature.data.edit_bones.new(name)\r\n bone.head = positions[i]\r\n bone.tail = positions[i + 1]\r\n \r\n if prev_bone:\r\n parent = armature.data.edit_bones.get(prev_bone)\r\n if parent:\r\n bone.parent = parent\r\n bone.use_connect = (i > 0 or parent_bone is None)\r\n \r\n bone_names.append(name)\r\n prev_bone = name\r\n \r\n return bone_names\r\n\r\n\r\ndef add_bone_constraint(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n constraint_type: str,\r\n target: bpy.types.Object = None,\r\n subtarget: str = None,\r\n **kwargs\r\n) -> bpy.types.Constraint:\r\n \"\"\"\r\n Add a constraint to a pose bone.\r\n \r\n Args:\r\n armature: Armature object\r\n bone_name: Target bone name\r\n constraint_type: 'IK', 'COPY_ROTATION', 'LIMIT_ROTATION', 'DAMPED_TRACK', etc.\r\n target: Target object\r\n subtarget: Target bone name (for armature targets)\r\n **kwargs: Additional constraint settings\r\n \r\n Returns:\r\n The created constraint\r\n \r\n Example:\r\n >>> add_bone_constraint(arm, \"Forearm.L\", \"IK\", arm, \"HandTarget.L\", chain_count=2)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n pose_bone = armature.pose.bones.get(bone_name)\r\n if not pose_bone:\r\n raise ValueError(f\"Bone '{bone_name}' not found\")\r\n \r\n constraint = pose_bone.constraints.new(constraint_type)\r\n \r\n if target:\r\n constraint.target = target\r\n if subtarget and hasattr(constraint, 'subtarget'):\r\n constraint.subtarget = subtarget\r\n \r\n for key, value in kwargs.items():\r\n if hasattr(constraint, key):\r\n setattr(constraint, key, value)\r\n \r\n return constraint\r\n\r\n\r\ndef setup_ik_chain(\r\n armature: bpy.types.Object,\r\n tip_bone: str,\r\n chain_length: int,\r\n target_name: str = None,\r\n pole_target_name: str = None,\r\n pole_angle: float = 0\r\n) -> dict:\r\n \"\"\"\r\n Set up an IK chain on a bone.\r\n \r\n Args:\r\n armature: Armature object\r\n tip_bone: End bone of IK chain\r\n chain_length: Number of bones in chain\r\n target_name: Name for IK target bone (created if None)\r\n pole_target_name: Name for pole target bone (optional)\r\n pole_angle: Pole angle in degrees\r\n \r\n Returns:\r\n Dict with 'constraint', 'target', 'pole' keys\r\n \r\n Example:\r\n >>> setup = setup_ik_chain(arm, \"Forearm.L\", chain_length=2)\r\n \"\"\"\r\n result = {}\r\n \r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n # Get tip bone position\r\n edit_bone = armature.data.edit_bones.get(tip_bone)\r\n if not edit_bone:\r\n raise ValueError(f\"Bone '{tip_bone}' not found\")\r\n \r\n target_pos = edit_bone.tail.copy()\r\n \r\n # Create IK target bone\r\n if target_name is None:\r\n target_name = f\"{tip_bone}_IK\"\r\n \r\n target_bone = armature.data.edit_bones.new(target_name)\r\n target_bone.head = target_pos\r\n target_bone.tail = (target_pos[0], target_pos[1], target_pos[2] + 0.1)\r\n result['target'] = target_name\r\n \r\n # Create pole target if specified\r\n if pole_target_name:\r\n # Position pole in front of the joint\r\n mid_bone = edit_bone.parent\r\n if mid_bone:\r\n pole_pos = (mid_bone.head[0], mid_bone.head[1] - 0.5, mid_bone.head[2])\r\n pole_bone = armature.data.edit_bones.new(pole_target_name)\r\n pole_bone.head = pole_pos\r\n pole_bone.tail = (pole_pos[0], pole_pos[1], pole_pos[2] + 0.1)\r\n result['pole'] = pole_target_name\r\n \r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n # Add IK constraint\r\n pose_bone = armature.pose.bones.get(tip_bone)\r\n ik = pose_bone.constraints.new('IK')\r\n ik.target = armature\r\n ik.subtarget = target_name\r\n ik.chain_count = chain_length\r\n \r\n if pole_target_name and pole_target_name in [b.name for b in armature.data.edit_bones]:\r\n ik.pole_target = armature\r\n ik.pole_subtarget = pole_target_name\r\n ik.pole_angle = math.radians(pole_angle)\r\n \r\n result['constraint'] = ik\r\n \r\n return result\r\n\r\n\r\ndef copy_bone_transforms(armature: bpy.types.Object) -> None:\r\n \"\"\"Copy current pose as rest pose.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n bpy.ops.pose.armature_apply()\r\n\r\n\r\ndef reset_pose(armature: bpy.types.Object) -> None:\r\n \"\"\"Reset all bones to rest position.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n bpy.ops.pose.select_all(action='SELECT')\r\n bpy.ops.pose.transforms_clear()\r\n\r\n\r\ndef set_bone_roll(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n roll: float\r\n) -> None:\r\n \"\"\"\r\n Set bone roll angle.\r\n \r\n Args:\r\n armature: Armature object\r\n bone_name: Target bone\r\n roll: Roll angle in degrees\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n bone = armature.data.edit_bones.get(bone_name)\r\n if bone:\r\n bone.roll = math.radians(roll)\r\n\r\n\r\ndef create_simple_rig(\r\n armature_name: str = \"Armature\",\r\n location: tuple = (0, 0, 0)\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a new armature object.\r\n \r\n Args:\r\n armature_name: Name for the armature\r\n location: Position\r\n \r\n Returns:\r\n The created armature object\r\n \"\"\"\r\n bpy.ops.object.armature_add(location=location)\r\n armature = bpy.context.active_object\r\n armature.name = armature_name\r\n armature.show_in_front = True\r\n return armature\r\n\r\n\r\ndef parent_mesh_to_armature(\r\n mesh: bpy.types.Object,\r\n armature: bpy.types.Object,\r\n method: str = 'AUTOMATIC'\r\n) -> None:\r\n \"\"\"\r\n Parent mesh to armature with automatic weights.\r\n \r\n Args:\r\n mesh: Mesh object\r\n armature: Armature object\r\n method: 'AUTOMATIC', 'ENVELOPE', or 'EMPTY'\r\n \"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n mesh.select_set(True)\r\n armature.select_set(True)\r\n bpy.context.view_layer.objects.active = armature\r\n \r\n type_map = {\r\n 'AUTOMATIC': 'ARMATURE_AUTO',\r\n 'ENVELOPE': 'ARMATURE_ENVELOPE',\r\n 'EMPTY': 'ARMATURE'\r\n }\r\n \r\n bpy.ops.object.parent_set(type=type_map.get(method, 'ARMATURE_AUTO'))","category":"utility","source_file":"armature_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Copy Bone Transforms. Category: utility","output":"import bpy\n\ndef copy_bone_transforms(armature: bpy.types.Object) -> None:\r\n \"\"\"Copy current pose as rest pose.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n bpy.ops.pose.armature_apply()\r\n\r\n\r\ndef reset_pose(armature: bpy.types.Object) -> None:\r\n \"\"\"Reset all bones to rest position.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n bpy.ops.pose.select_all(action='SELECT')\r\n bpy.ops.pose.transforms_clear()\r\n\r\n\r\ndef set_bone_roll(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n roll: float\r\n) -> None:\r\n \"\"\"\r\n Set bone roll angle.\r\n \r\n Args:\r\n armature: Armature object\r\n bone_name: Target bone\r\n roll: Roll angle in degrees\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n bone = armature.data.edit_bones.get(bone_name)\r\n if bone:\r\n bone.roll = math.radians(roll)\r\n\r\n\r\ndef create_simple_rig(\r\n armature_name: str = \"Armature\",\r\n location: tuple = (0, 0, 0)\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a new armature object.\r\n \r\n Args:\r\n armature_name: Name for the armature\r\n location: Position\r\n \r\n Returns:\r\n The created armature object\r\n \"\"\"\r\n bpy.ops.object.armature_add(location=location)\r\n armature = bpy.context.active_object\r\n armature.name = armature_name\r\n armature.show_in_front = True\r\n return armature\r\n\r\n\r\ndef parent_mesh_to_armature(\r\n mesh: bpy.types.Object,\r\n armature: bpy.types.Object,\r\n method: str = 'AUTOMATIC'\r\n) -> None:\r\n \"\"\"\r\n Parent mesh to armature with automatic weights.\r\n \r\n Args:\r\n mesh: Mesh object\r\n armature: Armature object\r\n method: 'AUTOMATIC', 'ENVELOPE', or 'EMPTY'\r\n \"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n mesh.select_set(True)\r\n armature.select_set(True)\r\n bpy.context.view_layer.objects.active = armature\r\n \r\n type_map = {\r\n 'AUTOMATIC': 'ARMATURE_AUTO',\r\n 'ENVELOPE': 'ARMATURE_ENVELOPE',\r\n 'EMPTY': 'ARMATURE'\r\n }\r\n \r\n bpy.ops.object.parent_set(type=type_map.get(method, 'ARMATURE_AUTO'))","category":"utility","source_file":"armature_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Batch Operations Utilities. Functions for performing operations on multiple objects.","output":"import bpy\r\n\r\n\r\ndef batch_rename(\r\n prefix: str = None,\r\n suffix: str = None,\r\n replace_from: str = None,\r\n replace_to: str = None,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"\r\n Batch rename objects.\r\n \r\n Args:\r\n prefix: Add prefix to names\r\n suffix: Add suffix to names\r\n replace_from: Text to replace\r\n replace_to: Replacement text\r\n selection_only: Only selected objects\r\n \r\n Returns:\r\n Number of objects renamed\r\n \"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else bpy.data.objects\r\n \r\n for obj in objects:\r\n new_name = obj.name\r\n \r\n if replace_from and replace_to is not None:\r\n new_name = new_name.replace(replace_from, replace_to)\r\n \r\n if prefix:\r\n new_name = prefix + new_name\r\n \r\n if suffix:\r\n new_name = new_name + suffix\r\n \r\n if new_name != obj.name:\r\n obj.name = new_name\r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_apply_modifier(\r\n modifier_type: str,\r\n selection_only: bool = True,\r\n **modifier_settings\r\n) -> int:\r\n \"\"\"\r\n Add modifier to multiple objects.\r\n \r\n Args:\r\n modifier_type: Modifier type name\r\n selection_only: Only selected objects\r\n **modifier_settings: Modifier-specific settings\r\n \r\n Returns:\r\n Number of objects modified\r\n \"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else bpy.data.objects\r\n \r\n for obj in objects:\r\n if obj.type != 'MESH':\r\n continue\r\n \r\n mod = obj.modifiers.new(modifier_type, modifier_type.upper())\r\n \r\n for key, value in modifier_settings.items():\r\n if hasattr(mod, key):\r\n setattr(mod, key, value)\r\n \r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_apply_material(\r\n material: bpy.types.Material,\r\n selection_only: bool = True,\r\n replace_all: bool = False\r\n) -> int:\r\n \"\"\"\r\n Apply material to multiple objects.\r\n \r\n Args:\r\n material: Material to apply\r\n selection_only: Only selected objects\r\n replace_all: Replace all materials or just add\r\n \r\n Returns:\r\n Number of objects modified\r\n \"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else bpy.data.objects\r\n \r\n for obj in objects:\r\n if obj.type != 'MESH':\r\n continue\r\n \r\n if replace_all:\r\n obj.data.materials.clear()\r\n \r\n obj.data.materials.append(material)\r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_set_smooth(\r\n smooth: bool = True,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"\r\n Set smooth/flat shading for multiple objects.\r\n \r\n Returns:\r\n Number of objects modified\r\n \"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else bpy.data.objects\r\n \r\n for obj in objects:\r\n if obj.type != 'MESH':\r\n continue\r\n \r\n for poly in obj.data.polygons:\r\n poly.use_smooth = smooth\r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_origin_to_geometry(selection_only: bool = True) -> int:\r\n \"\"\"Set origin to geometry center for multiple objects.\"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else bpy.data.objects\r\n \r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n for obj in objects:\r\n if obj.type != 'MESH':\r\n continue\r\n \r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')\r\n obj.select_set(False)\r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_apply_transforms(\r\n location: bool = True,\r\n rotation: bool = True,\r\n scale: bool = True,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"Apply transforms to multiple objects.\"\"\"\r\n objects = bpy.context.selected_objects if selection_only else list(bpy.data.objects)\r\n \r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n for obj in objects:\r\n obj.select_set(True)\r\n \r\n bpy.ops.object.transform_apply(\r\n location=location,\r\n rotation=rotation,\r\n scale=scale\r\n )\r\n \r\n return len(objects)\r\n\r\n\r\ndef batch_convert_to_mesh(selection_only: bool = True) -> int:\r\n \"\"\"Convert curves/text/etc to mesh for multiple objects.\"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else list(bpy.data.objects)\r\n \r\n for obj in objects:\r\n if obj.type in ['CURVE', 'FONT', 'SURFACE', 'META']:\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.convert(target='MESH')\r\n obj.select_set(False)\r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_set_property(\r\n property_path: str,\r\n value,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"\r\n Set property on multiple objects.\r\n \r\n Args:\r\n property_path: Property path (e.g., 'display_type', 'hide_render')\r\n value: Value to set\r\n selection_only: Only selected objects\r\n \r\n Returns:\r\n Number of objects modified\r\n \"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else bpy.data.objects\r\n \r\n for obj in objects:\r\n try:\r\n setattr(obj, property_path, value)\r\n count += 1\r\n except:\r\n pass\r\n \r\n return count\r\n\r\n\r\ndef batch_parent_to(\r\n parent: bpy.types.Object,\r\n keep_transform: bool = True,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"Parent multiple objects to one parent.\"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else list(bpy.data.objects)\r\n \r\n for obj in objects:\r\n if obj == parent:\r\n continue\r\n \r\n if keep_transform:\r\n matrix = obj.matrix_world.copy()\r\n \r\n obj.parent = parent\r\n \r\n if keep_transform:\r\n obj.matrix_world = matrix\r\n \r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_unparent(\r\n keep_transform: bool = True,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"Unparent multiple objects.\"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else list(bpy.data.objects)\r\n \r\n for obj in objects:\r\n if obj.parent:\r\n if keep_transform:\r\n matrix = obj.matrix_world.copy()\r\n \r\n obj.parent = None\r\n \r\n if keep_transform:\r\n obj.matrix_world = matrix\r\n \r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef foreach_selected(callback, *args, **kwargs) -> list:\r\n \"\"\"\r\n Execute callback function on each selected object.\r\n \r\n Args:\r\n callback: Function to call with (object, *args, **kwargs)\r\n \r\n Returns:\r\n List of callback return values\r\n \"\"\"\r\n results = []\r\n for obj in bpy.context.selected_objects:\r\n result = callback(obj, *args, **kwargs)\r\n results.append(result)\r\n return results","category":"utility","source_file":"batch_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Batch Origin To Geometry. Category: utility","output":"import bpy\n\ndef batch_origin_to_geometry(selection_only: bool = True) -> int:\r\n \"\"\"Set origin to geometry center for multiple objects.\"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else bpy.data.objects\r\n \r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n for obj in objects:\r\n if obj.type != 'MESH':\r\n continue\r\n \r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')\r\n obj.select_set(False)\r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_apply_transforms(\r\n location: bool = True,\r\n rotation: bool = True,\r\n scale: bool = True,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"Apply transforms to multiple objects.\"\"\"\r\n objects = bpy.context.selected_objects if selection_only else list(bpy.data.objects)\r\n \r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n for obj in objects:\r\n obj.select_set(True)\r\n \r\n bpy.ops.object.transform_apply(\r\n location=location,\r\n rotation=rotation,\r\n scale=scale\r\n )\r\n \r\n return len(objects)\r\n\r\n\r\ndef batch_convert_to_mesh(selection_only: bool = True) -> int:\r\n \"\"\"Convert curves/text/etc to mesh for multiple objects.\"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else list(bpy.data.objects)\r\n \r\n for obj in objects:\r\n if obj.type in ['CURVE', 'FONT', 'SURFACE', 'META']:\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.convert(target='MESH')\r\n obj.select_set(False)\r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_set_property(\r\n property_path: str,\r\n value,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"\r\n Set property on multiple objects.\r\n \r\n Args:\r\n property_path: Property path (e.g., 'display_type', 'hide_render')\r\n value: Value to set\r\n selection_only: Only selected objects\r\n \r\n Returns:\r\n Number of objects modified\r\n \"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else bpy.data.objects\r\n \r\n for obj in objects:\r\n try:\r\n setattr(obj, property_path, value)\r\n count += 1\r\n except:\r\n pass\r\n \r\n return count\r\n\r\n\r\ndef batch_parent_to(\r\n parent: bpy.types.Object,\r\n keep_transform: bool = True,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"Parent multiple objects to one parent.\"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else list(bpy.data.objects)\r\n \r\n for obj in objects:\r\n if obj == parent:\r\n continue\r\n \r\n if keep_transform:\r\n matrix = obj.matrix_world.copy()\r\n \r\n obj.parent = parent\r\n \r\n if keep_transform:\r\n obj.matrix_world = matrix\r\n \r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef batch_unparent(\r\n keep_transform: bool = True,\r\n selection_only: bool = True\r\n) -> int:\r\n \"\"\"Unparent multiple objects.\"\"\"\r\n count = 0\r\n objects = bpy.context.selected_objects if selection_only else list(bpy.data.objects)\r\n \r\n for obj in objects:\r\n if obj.parent:\r\n if keep_transform:\r\n matrix = obj.matrix_world.copy()\r\n \r\n obj.parent = None\r\n \r\n if keep_transform:\r\n obj.matrix_world = matrix\r\n \r\n count += 1\r\n \r\n return count\r\n\r\n\r\ndef foreach_selected(callback, *args, **kwargs) -> list:\r\n \"\"\"\r\n Execute callback function on each selected object.\r\n \r\n Args:\r\n callback: Function to call with (object, *args, **kwargs)\r\n \r\n Returns:\r\n List of callback return values\r\n \"\"\"\r\n results = []\r\n for obj in bpy.context.selected_objects:\r\n result = callback(obj, *args, **kwargs)\r\n results.append(result)\r\n return results","category":"utility","source_file":"batch_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Blender Python API β€” Common Pitfalls & Correct Solutions. This module documents the most common Python scripting pitfalls in Blender with specific code patterns, error messages, and correct solutions.","output":"import bpy\r\nimport bmesh\r\nimport numpy as np\r\nfrom mathutils import Vector\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 1: The Context Trap β€” bpy.ops vs bpy.data\r\n# =============================================================================\r\n# Problem: bpy.ops operators rely on the active context (3D Viewport, selection).\r\n# They FAIL in headless/background mode or when wrong panel is active.\r\n# Solution: Use the \"Factory Pattern\" via bpy.data for all creation operations.\r\n\r\ndef create_object_WRONG():\r\n \"\"\"INCORRECT β€” relies on context, fails headlessly.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))\r\n return bpy.context.active_object # Fragile!\r\n\r\ndef create_object_CORRECT(name=\"MyObject\"):\r\n \"\"\"CORRECT β€” Factory Pattern via bpy.data. Always works.\"\"\"\r\n mesh = bpy.data.meshes.new(name=f\"{name}_Mesh\")\r\n obj = bpy.data.objects.new(name=name, object_data=mesh)\r\n bpy.context.scene.collection.objects.link(obj)\r\n return obj\r\n\r\ndef create_light_CORRECT(name, light_type, energy, color=(1, 1, 1)):\r\n \"\"\"CORRECT β€” Factory Pattern for lights. No context dependency.\"\"\"\r\n light_data = bpy.data.lights.new(name=name, type=light_type)\r\n light_data.energy = energy\r\n light_data.color = color # 3-tuple RGB, NOT 4-tuple RGBA!\r\n light_obj = bpy.data.objects.new(name=name, object_data=light_data)\r\n bpy.context.collection.objects.link(light_obj)\r\n return light_obj\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 2: Mesh Validation β€” Missing validate() and calc_edges\r\n# =============================================================================\r\n# Problem: from_pydata with invalid indices (face references vertex #5 when\r\n# only 4 exist) can CRASH Blender. Missing calc_edges = invisible edges.\r\n# Solution: ALWAYS call mesh.validate() and mesh.update(calc_edges=True).\r\n\r\ndef create_mesh_CORRECT(name, verts, edges, faces):\r\n \"\"\"CORRECT β€” Always validate after from_pydata.\"\"\"\r\n mesh = bpy.data.meshes.new(name)\r\n mesh.from_pydata(verts, edges, faces)\r\n mesh.validate(verbose=True) # Auto-corrects invalid geometry\r\n mesh.update(calc_edges=True) # Recalculates internal edge data\r\n obj = bpy.data.objects.new(name, mesh)\r\n bpy.context.scene.collection.objects.link(obj)\r\n return obj\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 3: Light Color β€” 3-tuple RGB, NOT 4-tuple RGBA\r\n# =============================================================================\r\n# Problem: bpy.types.Light strictly accepts 3-item RGB arrays.\r\n# Providing 4 values (RGBA) raises an error or is silently ignored.\r\n# Solution: Always use (R, G, B) for light colors.\r\n\r\ndef set_light_color_WRONG(light_obj):\r\n \"\"\"INCORRECT β€” 4 values for light color.\"\"\"\r\n light_obj.data.color = (1.0, 0.0, 0.0, 1.0) # FAILS!\r\n\r\ndef set_light_color_CORRECT(light_obj):\r\n \"\"\"CORRECT β€” 3 values for light color.\"\"\"\r\n light_obj.data.color = (1.0, 0.0, 0.0) # Red, 3-tuple only\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 4: Light Energy Units β€” Watts vs W/mΒ²\r\n# =============================================================================\r\n# - Point, Spot, Area lights: energy in WATTS (e.g., 500W)\r\n# - Sun lights: energy in WATTS PER SQUARE METER (e.g., 5 W/mΒ²)\r\n# - Real sunlight β‰ˆ 441 W/mΒ² β€” DO NOT set sun to 1000 (apocalyptic!)\r\n# - Emission shaders on meshes: also in W/mΒ²\r\n\r\ndef create_sun_CORRECT():\r\n \"\"\"CORRECT β€” Sun uses W/mΒ², not Watts. Typical value: 3-10 W/mΒ².\"\"\"\r\n sun = create_light_CORRECT(\"Sun\", 'SUN', energy=5.0) # 5 W/mΒ²\r\n return sun\r\n\r\ndef create_area_light_CORRECT():\r\n \"\"\"CORRECT β€” Area light uses Watts. Typical value: 200-1000W.\"\"\"\r\n area = create_light_CORRECT(\"AreaLight\", 'AREA', energy=500) # 500 Watts\r\n area.data.size = 2.0 # Larger = softer shadows\r\n return area\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 5: ShaderNodeMix β€” Must Set data_type Explicitly\r\n# =============================================================================\r\n# Problem: ShaderNodeMixRGB was REMOVED in Blender 4.0.\r\n# ShaderNodeMix defaults to FLOAT, not color mixing.\r\n# Solution: Set data_type = 'RGBA' explicitly after creation.\r\n\r\ndef create_mix_node_WRONG(nodes):\r\n \"\"\"INCORRECT β€” defaults to FLOAT, won't mix colors.\"\"\"\r\n mix = nodes.new('ShaderNodeMix') # data_type defaults to 'FLOAT'!\r\n return mix\r\n\r\ndef create_mix_node_CORRECT(nodes):\r\n \"\"\"CORRECT β€” explicitly set RGBA for color mixing.\"\"\"\r\n mix = nodes.new('ShaderNodeMix')\r\n mix.data_type = 'RGBA' # CRITICAL: Must be set explicitly\r\n mix.blend_type = 'MIX' # Or 'MULTIPLY', 'ADD', etc.\r\n # Inputs: mix.inputs[0] = Factor, mix.inputs[6] = A, mix.inputs[7] = B\r\n # Output: mix.outputs[2] = Result Color\r\n return mix\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 6: Principled BSDF Socket Renaming (4.0+)\r\n# =============================================================================\r\n# Problem: Scripts using old socket names get KeyError.\r\n# Solution: Use the new OpenPBR-compliant names.\r\n#\r\n# SOCKET NAME MAPPING:\r\n# | OLD (3.x) | NEW (4.0/5.0) |\r\n# |---------------------|--------------------------|\r\n# | \"Subsurface\" | \"Subsurface Weight\" |\r\n# | \"Subsurface Color\" | REMOVED (use Base Color) |\r\n# | \"Specular\" | \"Specular IOR Level\" |\r\n# | \"Transmission\" | \"Transmission Weight\" |\r\n# | \"Clearcoat\" | \"Coat Weight\" |\r\n# | \"Sheen\" | \"Sheen Weight\" |\r\n# | \"Emission\" | \"Emission Color\" |\r\n# | \"Base Color\" | \"Base Color\" (unchanged) |\r\n# | \"Metallic\" | \"Metallic\" (unchanged) |\r\n# | \"Roughness\" | \"Roughness\" (unchanged) |\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 7: use_nodes Deprecation (5.0)\r\n# =============================================================================\r\n# Problem: material.use_nodes = True is deprecated. Always returns True.\r\n# Harmless now but will be REMOVED in Blender 6.0.\r\n# Solution: Remove use_nodes from scripts, proceed directly to node_tree.\r\n\r\ndef create_material_CORRECT(name):\r\n \"\"\"CORRECT β€” Skip use_nodes, go straight to node_tree.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n # mat.use_nodes = True # DEPRECATED in 5.0, always True\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n return mat, nodes, links\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 8: BMesh β€” ensure_lookup_table() Required\r\n# =============================================================================\r\n# Problem: After creating/deleting geometry in BMesh, internal index table\r\n# becomes outdated. Accessing by index causes IndexError.\r\n# Solution: Call ensure_lookup_table() after geometry changes.\r\n\r\ndef bmesh_example_CORRECT(mesh):\r\n \"\"\"CORRECT β€” ensure lookup tables after changes, free when done.\"\"\"\r\n bm = bmesh.new()\r\n bm.from_mesh(mesh)\r\n\r\n # Create geometry...\r\n bmesh.ops.create_cube(bm, size=1.0)\r\n\r\n # CRITICAL: Update lookup tables after geometry changes\r\n bm.verts.ensure_lookup_table()\r\n bm.edges.ensure_lookup_table()\r\n bm.faces.ensure_lookup_table()\r\n\r\n # Now safe to access by index\r\n first_vert = bm.verts[0]\r\n\r\n # Write back and ALWAYS free\r\n bm.to_mesh(mesh)\r\n bm.free() # CRITICAL: Prevent memory leaks\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 9: NumPy foreach_set β€” Must Flatten Arrays\r\n# =============================================================================\r\n# Problem: foreach_set expects a flat 1D array. Passing Nx3 array fails.\r\n# Solution: Always .flatten() before passing to Blender.\r\n\r\ndef set_vertices_numpy_CORRECT(mesh, positions_nx3):\r\n \"\"\"CORRECT β€” Flatten Nx3 array before foreach_set.\"\"\"\r\n flat = positions_nx3.flatten() # CRITICAL: Must be 1D\r\n mesh.vertices.foreach_set(\"co\", flat)\r\n mesh.update()\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 10: Boolean Solver β€” \"FAST\" Renamed to \"FLOAT\" in 5.0\r\n# =============================================================================\r\n# Problem: FAST solver doesn't exist in 5.0. Only EXACT, FLOAT, MANIFOLD.\r\n# Solution: Use FLOAT for speed, EXACT for precision.\r\n\r\ndef add_boolean_modifier_CORRECT(obj, cutter, operation='DIFFERENCE'):\r\n \"\"\"CORRECT β€” Use FLOAT solver (not FAST) in Blender 5.0.\"\"\"\r\n mod = obj.modifiers.new(name=\"Boolean\", type='BOOLEAN')\r\n mod.operation = operation\r\n mod.object = cutter\r\n mod.solver = 'FLOAT' # Was 'FAST' pre-5.0, now 'FLOAT'\r\n return mod\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 11: Grease Pencil β†’ Annotation (5.0)\r\n# =============================================================================\r\n# Problem: GreasePencil types renamed to Annotation in 5.0.\r\n# Solution: Use new names:\r\n# bpy.types.GreasePencil β†’ bpy.types.Annotation\r\n# bpy.data.grease_pencils β†’ bpy.types.annotations\r\n# GPencilStroke β†’ AnnotationStroke\r\n# GPencilLayer β†’ AnnotationLayer\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 12: EEVEE Engine Rename (5.0)\r\n# =============================================================================\r\n# Problem: BLENDER_EEVEE_NEXT renamed to BLENDER_EEVEE in 5.0.\r\n# Solution: Use 'BLENDER_EEVEE' for the render engine enum.\r\n\r\ndef set_eevee_CORRECT():\r\n \"\"\"CORRECT β€” EEVEE engine name for Blender 5.0.\"\"\"\r\n bpy.context.scene.render.engine = 'BLENDER_EEVEE'\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 13: Dictionary Property Access Removed (5.0)\r\n# =============================================================================\r\n# Problem: scene['cycles'] no longer works. Properties defined via bpy.props\r\n# are no longer accessible via dictionary syntax.\r\n# Solution: Use attribute access directly.\r\n\r\ndef access_cycles_WRONG():\r\n \"\"\"INCORRECT β€” dict access removed in 5.0.\"\"\"\r\n val = bpy.context.scene['cycles'] # TypeError in 5.0!\r\n\r\ndef access_cycles_CORRECT():\r\n \"\"\"CORRECT β€” Use attribute access.\"\"\"\r\n bpy.context.scene.render.engine = 'CYCLES'\r\n bpy.context.scene.cycles.samples = 128\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 14: Mathutils Float Precision (5.0)\r\n# =============================================================================\r\n# Problem: mathutils.Vector now uses float32 (was float64).\r\n# Scientific scripts relying on double precision may see drift.\r\n# Solution: Be aware; cast to float64 if precision matters.\r\n\r\ndef precision_aware_calculation():\r\n \"\"\"Note: Blender 5.0 mathutils uses float32 by default.\"\"\"\r\n v = Vector((0.1, 0.2, 0.3)) # float32 in 5.0\r\n # For high-precision: convert to numpy float64\r\n precise = np.array(v, dtype=np.float64)\r\n return precise\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 15: Context Overrides β€” Use temp_override (3.2+)\r\n# =============================================================================\r\n# Problem: Old dict-based context overrides are deprecated.\r\n# Solution: Use bpy.context.temp_override() for Blender 3.2+.\r\n\r\ndef apply_modifier_CORRECT(obj, modifier_name):\r\n \"\"\"CORRECT β€” Use temp_override for operator context.\"\"\"\r\n with bpy.context.temp_override(object=obj):\r\n bpy.ops.object.modifier_apply(modifier=modifier_name)\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 16: bpy.data.textures vs Shader Node Inputs\r\n# =============================================================================\r\n# Problem: bpy.data.textures (modifier textures) use PROPERTIES like .noise_scale.\r\n# Shader nodes (ShaderNodeTexNoise etc.) use INPUTS like inputs['Scale'].\r\n# These are TWO COMPLETELY DIFFERENT APIs for textures.\r\n# Error: AttributeError: 'NoiseTexture' object has no attribute 'noise_scale'\r\n\r\ndef set_noise_scale_WRONG(noise_node):\r\n \"\"\"INCORRECT β€” noise_scale is a bpy.data.textures property, not a shader node.\"\"\"\r\n noise_node.noise_scale = 5.0 # AttributeError!\r\n\r\ndef set_noise_scale_CORRECT(noise_node):\r\n \"\"\"CORRECT β€” Shader nodes use inputs['Scale'].default_value.\"\"\"\r\n noise_node.inputs['Scale'].default_value = 5.0 # Correct for shader nodes\r\n\r\ndef set_modifier_texture_scale_CORRECT():\r\n \"\"\"CORRECT β€” bpy.data.textures use .noise_scale property directly.\"\"\"\r\n tex = bpy.data.textures.new(name=\"MyNoise\", type='NOISE')\r\n tex.noise_scale = 0.5 # Correct for bpy.data.textures (modifier textures)\r\n\r\n\r\n# =============================================================================\r\n# PITFALL 17: Camera X Rotation Direction\r\n# =============================================================================\r\n# Problem: Camera Euler X rotation is often set incorrectly when aiming up or down.\r\n# X=90Β° (radians: pi/2) = perfectly horizontal, looking straight ahead.\r\n# X < 90Β° = looking DOWN. X > 90Β° = looking UP.\r\n# Solution: Use math.radians() and remember: >90Β° = up, <90Β° = down.\r\n\r\nimport math\r\n\r\ndef set_camera_looking_slightly_up(camera_obj):\r\n \"\"\"CORRECT β€” X=95Β° means looking 5Β° above horizontal.\"\"\"\r\n camera_obj.rotation_euler = (math.radians(95), 0, 0)\r\n\r\ndef set_camera_looking_slightly_down(camera_obj):\r\n \"\"\"CORRECT β€” X=80Β° means looking 10Β° below horizontal.\"\"\"\r\n camera_obj.rotation_euler = (math.radians(80), 0, 0)\r\n\r\ndef set_camera_looking_horizontal(camera_obj):\r\n \"\"\"CORRECT β€” X=90Β° = perfectly horizontal.\"\"\"\r\n camera_obj.rotation_euler = (math.radians(90), 0, 0)","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: INCORRECT β€” relies on context, fails headlessly.","output":"import bpy\n\ndef create_object_WRONG():\n \"\"\"INCORRECT β€” relies on context, fails headlessly.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))\r\n return bpy.context.active_object # Fragile!","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Factory Pattern via bpy.data. Always works.","output":"import bpy\n\ndef create_object_CORRECT(name=\"MyObject\"):\n \"\"\"CORRECT β€” Factory Pattern via bpy.data. Always works.\"\"\"\r\n mesh = bpy.data.meshes.new(name=f\"{name}_Mesh\")\r\n obj = bpy.data.objects.new(name=name, object_data=mesh)\r\n bpy.context.scene.collection.objects.link(obj)\r\n return obj","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Factory Pattern for lights. No context dependency.","output":"import bpy\n\ndef create_light_CORRECT(name, light_type, energy, color=(1, 1, 1)):\n \"\"\"CORRECT β€” Factory Pattern for lights. No context dependency.\"\"\"\r\n light_data = bpy.data.lights.new(name=name, type=light_type)\r\n light_data.energy = energy\r\n light_data.color = color # 3-tuple RGB, NOT 4-tuple RGBA!\r\n light_obj = bpy.data.objects.new(name=name, object_data=light_data)\r\n bpy.context.collection.objects.link(light_obj)\r\n return light_obj","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Always validate after from_pydata.","output":"import bpy\n\ndef create_mesh_CORRECT(name, verts, edges, faces):\n \"\"\"CORRECT β€” Always validate after from_pydata.\"\"\"\r\n mesh = bpy.data.meshes.new(name)\r\n mesh.from_pydata(verts, edges, faces)\r\n mesh.validate(verbose=True) # Auto-corrects invalid geometry\r\n mesh.update(calc_edges=True) # Recalculates internal edge data\r\n obj = bpy.data.objects.new(name, mesh)\r\n bpy.context.scene.collection.objects.link(obj)\r\n return obj","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: INCORRECT β€” 4 values for light color.","output":"import bpy\n\ndef set_light_color_WRONG(light_obj):\n \"\"\"INCORRECT β€” 4 values for light color.\"\"\"\r\n light_obj.data.color = (1.0, 0.0, 0.0, 1.0) # FAILS!","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” 3 values for light color.","output":"import bpy\n\ndef set_light_color_CORRECT(light_obj):\n \"\"\"CORRECT β€” 3 values for light color.\"\"\"\r\n light_obj.data.color = (1.0, 0.0, 0.0) # Red, 3-tuple only","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Sun uses W/mΒ², not Watts. Typical value: 3-10 W/mΒ².","output":"import bpy\n\ndef create_sun_CORRECT():\n \"\"\"CORRECT β€” Sun uses W/mΒ², not Watts. Typical value: 3-10 W/mΒ².\"\"\"\r\n sun = create_light_CORRECT(\"Sun\", 'SUN', energy=5.0) # 5 W/mΒ²\r\n return sun","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Area light uses Watts. Typical value: 200-1000W.","output":"import bpy\n\ndef create_area_light_CORRECT():\n \"\"\"CORRECT β€” Area light uses Watts. Typical value: 200-1000W.\"\"\"\r\n area = create_light_CORRECT(\"AreaLight\", 'AREA', energy=500) # 500 Watts\r\n area.data.size = 2.0 # Larger = softer shadows\r\n return area","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: INCORRECT β€” defaults to FLOAT, won't mix colors.","output":"import bpy\n\ndef create_mix_node_WRONG(nodes):\n \"\"\"INCORRECT β€” defaults to FLOAT, won't mix colors.\"\"\"\r\n mix = nodes.new('ShaderNodeMix') # data_type defaults to 'FLOAT'!\r\n return mix","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” explicitly set RGBA for color mixing.","output":"import bpy\n\ndef create_mix_node_CORRECT(nodes):\n \"\"\"CORRECT β€” explicitly set RGBA for color mixing.\"\"\"\r\n mix = nodes.new('ShaderNodeMix')\r\n mix.data_type = 'RGBA' # CRITICAL: Must be set explicitly\r\n mix.blend_type = 'MIX' # Or 'MULTIPLY', 'ADD', etc.\r\n # Inputs: mix.inputs[0] = Factor, mix.inputs[6] = A, mix.inputs[7] = B\r\n # Output: mix.outputs[2] = Result Color\r\n return mix","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Skip use_nodes, go straight to node_tree.","output":"import bpy\n\ndef create_material_CORRECT(name):\n \"\"\"CORRECT β€” Skip use_nodes, go straight to node_tree.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n # mat.use_nodes = True # DEPRECATED in 5.0, always True\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n return mat, nodes, links","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” ensure lookup tables after changes, free when done.","output":"import bpy\n\ndef bmesh_example_CORRECT(mesh):\n \"\"\"CORRECT β€” ensure lookup tables after changes, free when done.\"\"\"\r\n bm = bmesh.new()\r\n bm.from_mesh(mesh)\r\n\r\n # Create geometry...\r\n bmesh.ops.create_cube(bm, size=1.0)\r\n\r\n # CRITICAL: Update lookup tables after geometry changes\r\n bm.verts.ensure_lookup_table()\r\n bm.edges.ensure_lookup_table()\r\n bm.faces.ensure_lookup_table()\r\n\r\n # Now safe to access by index\r\n first_vert = bm.verts[0]\r\n\r\n # Write back and ALWAYS free\r\n bm.to_mesh(mesh)\r\n bm.free() # CRITICAL: Prevent memory leaks","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Flatten Nx3 array before foreach_set.","output":"import bpy\n\ndef set_vertices_numpy_CORRECT(mesh, positions_nx3):\n \"\"\"CORRECT β€” Flatten Nx3 array before foreach_set.\"\"\"\r\n flat = positions_nx3.flatten() # CRITICAL: Must be 1D\r\n mesh.vertices.foreach_set(\"co\", flat)\r\n mesh.update()","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Use FLOAT solver (not FAST) in Blender 5.0.","output":"import bpy\n\ndef add_boolean_modifier_CORRECT(obj, cutter, operation='DIFFERENCE'):\n \"\"\"CORRECT β€” Use FLOAT solver (not FAST) in Blender 5.0.\"\"\"\r\n mod = obj.modifiers.new(name=\"Boolean\", type='BOOLEAN')\r\n mod.operation = operation\r\n mod.object = cutter\r\n mod.solver = 'FLOAT' # Was 'FAST' pre-5.0, now 'FLOAT'\r\n return mod","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” EEVEE engine name for Blender 5.0.","output":"import bpy\n\ndef set_eevee_CORRECT():\n \"\"\"CORRECT β€” EEVEE engine name for Blender 5.0.\"\"\"\r\n bpy.context.scene.render.engine = 'BLENDER_EEVEE'","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: INCORRECT β€” dict access removed in 5.0.","output":"import bpy\n\ndef access_cycles_WRONG():\n \"\"\"INCORRECT β€” dict access removed in 5.0.\"\"\"\r\n val = bpy.context.scene['cycles'] # TypeError in 5.0!","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Use attribute access.","output":"import bpy\n\ndef access_cycles_CORRECT():\n \"\"\"CORRECT β€” Use attribute access.\"\"\"\r\n bpy.context.scene.render.engine = 'CYCLES'\r\n bpy.context.scene.cycles.samples = 128","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Note: Blender 5.0 mathutils uses float32 by default.","output":"import bpy\n\ndef precision_aware_calculation():\n \"\"\"Note: Blender 5.0 mathutils uses float32 by default.\"\"\"\r\n v = Vector((0.1, 0.2, 0.3)) # float32 in 5.0\r\n # For high-precision: convert to numpy float64\r\n precise = np.array(v, dtype=np.float64)\r\n return precise","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Use temp_override for operator context.","output":"import bpy\n\ndef apply_modifier_CORRECT(obj, modifier_name):\n \"\"\"CORRECT β€” Use temp_override for operator context.\"\"\"\r\n with bpy.context.temp_override(object=obj):\r\n bpy.ops.object.modifier_apply(modifier=modifier_name)","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: INCORRECT β€” noise_scale is a bpy.data.textures property, not a shader node.","output":"import bpy\n\ndef set_noise_scale_WRONG(noise_node):\n \"\"\"INCORRECT β€” noise_scale is a bpy.data.textures property, not a shader node.\"\"\"\r\n noise_node.noise_scale = 5.0 # AttributeError!","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” Shader nodes use inputs['Scale'].default_value.","output":"import bpy\n\ndef set_noise_scale_CORRECT(noise_node):\n \"\"\"CORRECT β€” Shader nodes use inputs['Scale'].default_value.\"\"\"\r\n noise_node.inputs['Scale'].default_value = 5.0 # Correct for shader nodes","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” bpy.data.textures use .noise_scale property directly.","output":"import bpy\n\ndef set_modifier_texture_scale_CORRECT():\n \"\"\"CORRECT β€” bpy.data.textures use .noise_scale property directly.\"\"\"\r\n tex = bpy.data.textures.new(name=\"MyNoise\", type='NOISE')\r\n tex.noise_scale = 0.5 # Correct for bpy.data.textures (modifier textures)","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” X=95Β° means looking 5Β° above horizontal.","output":"import bpy\n\ndef set_camera_looking_slightly_up(camera_obj):\n \"\"\"CORRECT β€” X=95Β° means looking 5Β° above horizontal.\"\"\"\r\n camera_obj.rotation_euler = (math.radians(95), 0, 0)","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” X=80Β° means looking 10Β° below horizontal.","output":"import bpy\n\ndef set_camera_looking_slightly_down(camera_obj):\n \"\"\"CORRECT β€” X=80Β° means looking 10Β° below horizontal.\"\"\"\r\n camera_obj.rotation_euler = (math.radians(80), 0, 0)","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: CORRECT β€” X=90Β° = perfectly horizontal.","output":"import bpy\n\ndef set_camera_looking_horizontal(camera_obj):\n \"\"\"CORRECT β€” X=90Β° = perfectly horizontal.\"\"\"\r\n camera_obj.rotation_euler = (math.radians(90), 0, 0)","category":"utility","source_file":"blender_api_pitfalls.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Bone Helper Utilities. Helper functions for bone manipulation and rigging.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef get_bone_chain(\r\n armature: bpy.types.Object,\r\n start_bone: str,\r\n end_bone: str\r\n) -> list:\r\n \"\"\"Get list of bones from start to end (following parent chain).\"\"\"\r\n bones = armature.data.bones\r\n chain = []\r\n \r\n current = bones.get(end_bone)\r\n while current:\r\n chain.insert(0, current.name)\r\n if current.name == start_bone:\r\n break\r\n current = current.parent\r\n \r\n return chain\r\n\r\n\r\ndef set_bone_roll(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n roll: float\r\n) -> None:\r\n \"\"\"Set bone roll angle in degrees.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n bone = armature.data.edit_bones.get(bone_name)\r\n if bone:\r\n bone.roll = math.radians(roll)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef align_bone_to_world(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n axis: str = 'Y'\r\n) -> None:\r\n \"\"\"Align bone to world axis.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n bone = armature.data.edit_bones.get(bone_name)\r\n if bone:\r\n length = bone.length\r\n head = bone.head.copy()\r\n \r\n if axis == 'X':\r\n bone.tail = head + (length, 0, 0)\r\n elif axis == 'Y':\r\n bone.tail = head + (0, length, 0)\r\n elif axis == 'Z':\r\n bone.tail = head + (0, 0, length)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef create_bone_group(\r\n armature: bpy.types.Object,\r\n group_name: str,\r\n bone_names: list,\r\n color_set: str = 'THEME01'\r\n) -> None:\r\n \"\"\"Create bone group with color.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n # In Blender 4.0+, use bone_collections\r\n if hasattr(armature.data, 'collections'):\r\n coll = armature.data.collections.new(group_name)\r\n for name in bone_names:\r\n if name in armature.pose.bones:\r\n coll.assign(armature.pose.bones[name])\r\n else:\r\n # Legacy bone groups\r\n group = armature.pose.bone_groups.new(name=group_name)\r\n group.color_set = color_set\r\n \r\n for name in bone_names:\r\n if name in armature.pose.bones:\r\n armature.pose.bones[name].bone_group = group\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef add_stretch_to(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n target_bone: str\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add stretch-to constraint.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n bone = armature.pose.bones.get(bone_name)\r\n if bone:\r\n constraint = bone.constraints.new('STRETCH_TO')\r\n constraint.target = armature\r\n constraint.subtarget = target_bone\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n return constraint\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n return None\r\n\r\n\r\ndef add_copy_bone_constraint(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n target_bone: str,\r\n constraint_type: str = 'COPY_ROTATION'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add copy constraint between bones.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n bone = armature.pose.bones.get(bone_name)\r\n if bone:\r\n constraint = bone.constraints.new(constraint_type)\r\n constraint.target = armature\r\n constraint.subtarget = target_bone\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n return constraint\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n return None\r\n\r\n\r\ndef set_bone_layer(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n layer: int\r\n) -> None:\r\n \"\"\"Move bone to specific layer (deprecated in 4.0+).\"\"\"\r\n if hasattr(armature.data.bones[bone_name], 'layers'):\r\n layers = [False] * 32\r\n layers[layer] = True\r\n armature.data.bones[bone_name].layers = layers\r\n\r\n\r\ndef mirror_bone_pose(\r\n armature: bpy.types.Object,\r\n bone_name: str\r\n) -> None:\r\n \"\"\"Mirror pose bone to opposite side.\"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n bone = armature.pose.bones.get(bone_name)\r\n if bone:\r\n bone.bone.select = True\r\n bpy.ops.pose.copy()\r\n bpy.ops.pose.paste(flipped=True)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')","category":"utility","source_file":"bone_utils.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Boolean Operations Utilities. Functions for boolean operations and CSG workflows.","output":"import bpy\r\n\r\n\r\ndef add_boolean_modifier(\r\n target: bpy.types.Object,\r\n cutter: bpy.types.Object,\r\n operation: str = 'DIFFERENCE',\r\n solver: str = 'EXACT',\r\n apply: bool = False\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Add boolean modifier.\r\n \r\n Args:\r\n target: Object to modify\r\n cutter: Boolean object\r\n operation: 'DIFFERENCE', 'UNION', 'INTERSECT'\r\n solver: 'EXACT', 'FAST'\r\n apply: Apply modifier immediately\r\n \r\n Returns:\r\n The boolean modifier (if not applied)\r\n \"\"\"\r\n mod = target.modifiers.new(f\"Boolean_{cutter.name}\", 'BOOLEAN')\r\n mod.operation = operation\r\n mod.solver = solver\r\n mod.object = cutter\r\n \r\n if apply:\r\n bpy.context.view_layer.objects.active = target\r\n bpy.ops.object.modifier_apply(modifier=mod.name)\r\n return None\r\n \r\n return mod\r\n\r\n\r\ndef boolean_difference(\r\n target: bpy.types.Object,\r\n cutter: bpy.types.Object,\r\n apply: bool = True,\r\n hide_cutter: bool = True\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Subtract cutter from target.\r\n \r\n Args:\r\n target: Object to cut\r\n cutter: Cutting object\r\n apply: Apply modifier\r\n hide_cutter: Hide cutter after operation\r\n \"\"\"\r\n mod = add_boolean_modifier(target, cutter, 'DIFFERENCE', apply=apply)\r\n \r\n if hide_cutter:\r\n cutter.hide_set(True)\r\n cutter.hide_render = True\r\n \r\n return mod\r\n\r\n\r\ndef boolean_union(\r\n target: bpy.types.Object,\r\n other: bpy.types.Object,\r\n apply: bool = True,\r\n delete_other: bool = False\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Combine two objects.\r\n \r\n Args:\r\n target: Base object\r\n other: Object to add\r\n apply: Apply modifier\r\n delete_other: Delete the other object after\r\n \"\"\"\r\n mod = add_boolean_modifier(target, other, 'UNION', apply=apply)\r\n \r\n if delete_other and apply:\r\n bpy.data.objects.remove(other)\r\n \r\n return mod\r\n\r\n\r\ndef boolean_intersect(\r\n target: bpy.types.Object,\r\n other: bpy.types.Object,\r\n apply: bool = True\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Keep only intersection of two objects.\r\n \r\n Args:\r\n target: Base object\r\n other: Intersecting object\r\n apply: Apply modifier\r\n \"\"\"\r\n return add_boolean_modifier(target, other, 'INTERSECT', apply=apply)\r\n\r\n\r\ndef boolean_slice(\r\n target: bpy.types.Object,\r\n cutter: bpy.types.Object,\r\n separate: bool = True\r\n) -> list:\r\n \"\"\"\r\n Slice object into two parts.\r\n \r\n Args:\r\n target: Object to slice\r\n cutter: Slicing plane/object\r\n separate: Create separate objects\r\n \r\n Returns:\r\n List of resulting objects\r\n \"\"\"\r\n # Duplicate for second half\r\n bpy.ops.object.select_all(action='DESELECT')\r\n target.select_set(True)\r\n bpy.context.view_layer.objects.active = target\r\n bpy.ops.object.duplicate()\r\n copy = bpy.context.active_object\r\n \r\n # Boolean difference on original\r\n boolean_difference(target, cutter, apply=True, hide_cutter=False)\r\n \r\n # Boolean intersect on copy\r\n boolean_intersect(copy, cutter, apply=True)\r\n \r\n cutter.hide_set(True)\r\n \r\n return [target, copy]\r\n\r\n\r\ndef create_boolean_cutter_cube(\r\n location: tuple = (0, 0, 0),\r\n size: tuple = (1, 1, 1),\r\n name: str = \"BoolCutter\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create cube for boolean operations.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=location)\r\n cutter = bpy.context.active_object\r\n cutter.name = name\r\n cutter.scale = (size[0]/2, size[1]/2, size[2]/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Set display for visibility\r\n cutter.display_type = 'WIRE'\r\n \r\n return cutter\r\n\r\n\r\ndef create_boolean_cutter_cylinder(\r\n location: tuple = (0, 0, 0),\r\n radius: float = 0.5,\r\n depth: float = 1.0,\r\n vertices: int = 32,\r\n name: str = \"BoolCutter\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create cylinder for boolean operations.\"\"\"\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius,\r\n depth=depth,\r\n vertices=vertices,\r\n location=location\r\n )\r\n cutter = bpy.context.active_object\r\n cutter.name = name\r\n cutter.display_type = 'WIRE'\r\n \r\n return cutter\r\n\r\n\r\ndef create_boolean_cutter_sphere(\r\n location: tuple = (0, 0, 0),\r\n radius: float = 0.5,\r\n segments: int = 32,\r\n name: str = \"BoolCutter\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create sphere for boolean operations.\"\"\"\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=radius,\r\n segments=segments,\r\n ring_count=segments // 2,\r\n location=location\r\n )\r\n cutter = bpy.context.active_object\r\n cutter.name = name\r\n cutter.display_type = 'WIRE'\r\n \r\n return cutter\r\n\r\n\r\ndef apply_all_booleans(obj: bpy.types.Object) -> int:\r\n \"\"\"\r\n Apply all boolean modifiers on object.\r\n \r\n Returns:\r\n Number of modifiers applied\r\n \"\"\"\r\n count = 0\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n for mod in obj.modifiers[:]:\r\n if mod.type == 'BOOLEAN':\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=mod.name)\r\n count += 1\r\n except:\r\n pass\r\n \r\n return count\r\n\r\n\r\ndef set_boolean_collection(\r\n target: bpy.types.Object,\r\n collection: bpy.types.Collection,\r\n operation: str = 'DIFFERENCE'\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Use collection as boolean operand.\r\n \r\n Args:\r\n target: Object to modify\r\n collection: Collection of cutter objects\r\n operation: Boolean operation\r\n \r\n Returns:\r\n The boolean modifier\r\n \"\"\"\r\n mod = target.modifiers.new(f\"Boolean_{collection.name}\", 'BOOLEAN')\r\n mod.operation = operation\r\n mod.operand_type = 'COLLECTION'\r\n mod.collection = collection\r\n mod.solver = 'EXACT'\r\n \r\n return mod\r\n\r\n\r\ndef cleanup_boolean_meshes(obj: bpy.types.Object) -> None:\r\n \"\"\"Clean up mesh after boolean operations.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n # Merge close vertices\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.mesh.remove_doubles(threshold=0.0001)\r\n \r\n # Remove internal faces\r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n bpy.ops.mesh.select_interior_faces()\r\n bpy.ops.mesh.delete(type='FACE')\r\n \r\n # Recalculate normals\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.mesh.normals_make_consistent(inside=False)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')","category":"utility","source_file":"boolean_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Apply All Booleans. Category: utility","output":"import bpy\n\ndef apply_all_booleans(obj: bpy.types.Object) -> int:\r\n \"\"\"\r\n Apply all boolean modifiers on object.\r\n \r\n Returns:\r\n Number of modifiers applied\r\n \"\"\"\r\n count = 0\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n for mod in obj.modifiers[:]:\r\n if mod.type == 'BOOLEAN':\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=mod.name)\r\n count += 1\r\n except:\r\n pass\r\n \r\n return count\r\n\r\n\r\ndef set_boolean_collection(\r\n target: bpy.types.Object,\r\n collection: bpy.types.Collection,\r\n operation: str = 'DIFFERENCE'\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Use collection as boolean operand.\r\n \r\n Args:\r\n target: Object to modify\r\n collection: Collection of cutter objects\r\n operation: Boolean operation\r\n \r\n Returns:\r\n The boolean modifier\r\n \"\"\"\r\n mod = target.modifiers.new(f\"Boolean_{collection.name}\", 'BOOLEAN')\r\n mod.operation = operation\r\n mod.operand_type = 'COLLECTION'\r\n mod.collection = collection\r\n mod.solver = 'EXACT'\r\n \r\n return mod\r\n\r\n\r\ndef cleanup_boolean_meshes(obj: bpy.types.Object) -> None:\r\n \"\"\"Clean up mesh after boolean operations.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n # Merge close vertices\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.mesh.remove_doubles(threshold=0.0001)\r\n \r\n # Remove internal faces\r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n bpy.ops.mesh.select_interior_faces()\r\n bpy.ops.mesh.delete(type='FACE')\r\n \r\n # Recalculate normals\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.mesh.normals_make_consistent(inside=False)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')","category":"utility","source_file":"boolean_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Camera Composition and Cinematography. Professional camera setup techniques for Blender scenes. Includes focal length presets, depth of field configuration, rule-of-thirds positioning, look-at tracking, and cinematic camera rigs. Essential for creating polished, film-quality renders.","output":"import bpy\r\nimport math\r\nfrom mathutils import Vector\r\n\r\n\r\n# =============================================================================\r\n# CAMERA COMPOSITION & CINEMATOGRAPHY\r\n# =============================================================================\r\n#\r\n# PROFESSIONAL CAMERA PRINCIPLES:\r\n# 1. Focal length sets the \"mood\":\r\n# - 24mm = dramatic, exaggerated perspective (action, interiors)\r\n# - 35mm = natural, documentary feel\r\n# - 50mm = closest to human eye, portraits\r\n# - 85mm = portrait lens, flattering compression\r\n# - 135mm+ = telephoto, compressed perspective (product shots)\r\n#\r\n# 2. Rule of thirds: place subjects at 1/3 or 2/3 of frame\r\n# 3. Camera height: eye level for neutral, low angle for power, high for overview\r\n# 4. DOF (Depth of Field): blurred background isolates the subject\r\n# 5. Always use a Track To constraint instead of manually computing rotations\r\n#\r\n# SENSOR SIZE (affects field of view):\r\n# Default Blender sensor: 36mm (full-frame equivalent)\r\n# For matching real cameras: set sensor_width accordingly\r\n# =============================================================================\r\n\r\n\r\n# --- FOCAL LENGTH PRESETS ---\r\nFOCAL_LENGTHS = {\r\n 'WIDE': 24, # Wide angle β€” interiors, landscapes, dramatic\r\n 'NORMAL': 35, # Standard β€” documentary, natural\r\n 'PORTRAIT': 50, # Human eye equivalent\r\n 'TELEPHOTO': 85, # Portrait, product, compressed perspective\r\n 'LONG': 135, # Product close-ups, extreme compression\r\n}\r\n\r\n\r\ndef setup_camera(\r\n location=(7, -7, 5),\r\n target=(0, 0, 0),\r\n focal_length=50,\r\n name=\"Camera\",\r\n clip_start=0.1,\r\n clip_end=1000,\r\n sensor_width=36\r\n):\r\n \"\"\"\r\n Create and configure a camera aimed at a target point.\r\n \r\n Uses a Track To constraint for automatic aiming (never manually\r\n compute rotation_euler for cameras β€” it's fragile and error-prone).\r\n \r\n Args:\r\n location: Camera position in world space\r\n target: Point the camera aims at (creates an Empty target)\r\n focal_length: Lens focal length in mm (see FOCAL_LENGTHS presets)\r\n name: Camera object name\r\n clip_start: Near clipping distance\r\n clip_end: Far clipping distance\r\n sensor_width: Sensor width in mm (36 = full-frame)\r\n \r\n Example:\r\n # Standard product shot\r\n cam = setup_camera(location=(5, -5, 3), target=(0, 0, 1), focal_length=85)\r\n \r\n # Dramatic wide interior\r\n cam = setup_camera(location=(3, -2, 1.6), target=(0, 3, 1), focal_length=24)\r\n \"\"\"\r\n # Create camera\r\n cam_data = bpy.data.cameras.new(name=name)\r\n cam_data.lens = focal_length\r\n cam_data.clip_start = clip_start\r\n cam_data.clip_end = clip_end\r\n cam_data.sensor_width = sensor_width\r\n \r\n cam_obj = bpy.data.objects.new(name=name, object_data=cam_data)\r\n bpy.context.collection.objects.link(cam_obj)\r\n cam_obj.location = location\r\n \r\n # Create target empty\r\n target_empty = bpy.data.objects.new(f\"{name}_Target\", None)\r\n bpy.context.collection.objects.link(target_empty)\r\n target_empty.location = target\r\n target_empty.empty_display_size = 0.2\r\n target_empty.empty_display_type = 'SPHERE'\r\n \r\n # Track To constraint β€” automatic aiming\r\n track = cam_obj.constraints.new(type='TRACK_TO')\r\n track.target = target_empty\r\n track.track_axis = 'TRACK_NEGATIVE_Z'\r\n track.up_axis = 'UP_Y'\r\n \r\n # Set as active camera\r\n bpy.context.scene.camera = cam_obj\r\n \r\n return cam_obj, target_empty\r\n\r\n\r\ndef setup_depth_of_field(\r\n camera_obj,\r\n focus_target=None,\r\n focus_distance=5.0,\r\n f_stop=2.8,\r\n aperture_blades=6\r\n):\r\n \"\"\"\r\n Configure depth of field for cinematic bokeh effect.\r\n \r\n Args:\r\n camera_obj: Camera object to configure\r\n focus_target: Object to focus on (overrides focus_distance if set)\r\n focus_distance: Manual focus distance in meters\r\n f_stop: Aperture f-stop (lower = more blur)\r\n 1.4 = extreme blur, 2.8 = moderate, 5.6 = slight, 16 = sharp\r\n aperture_blades: Number of blades for bokeh shape (0 = circle, 6 = hexagonal)\r\n \r\n Example:\r\n cam, target = setup_camera(location=(5, -5, 3), target=(0, 0, 1))\r\n setup_depth_of_field(cam, focus_distance=7.0, f_stop=2.0)\r\n \"\"\"\r\n cam_data = camera_obj.data\r\n cam_data.dof.use_dof = True\r\n \r\n if focus_target:\r\n cam_data.dof.focus_object = focus_target\r\n else:\r\n cam_data.dof.focus_distance = focus_distance\r\n \r\n cam_data.dof.aperture_fstop = f_stop\r\n cam_data.dof.aperture_blades = aperture_blades\r\n \r\n return cam_data\r\n\r\n\r\ndef position_camera_for_object(\r\n obj,\r\n distance=8.0,\r\n angle_degrees=30.0,\r\n height_offset=2.0,\r\n focal_length=50,\r\n camera_name=\"Camera\"\r\n):\r\n \"\"\"\r\n Automatically position a camera to frame a specific object.\r\n \r\n Places the camera at an orbit distance from the object, angled\r\n slightly above, with proper aiming via Track To constraint.\r\n \r\n Args:\r\n obj: Target object to frame\r\n distance: Distance from object center\r\n angle_degrees: Horizontal angle around the object (0 = front, 90 = side)\r\n height_offset: Camera height above object center\r\n focal_length: Lens focal length\r\n camera_name: Camera name\r\n \r\n Example:\r\n # Frame a table from 45Β° angle\r\n position_camera_for_object(bpy.data.objects[\"Table\"], distance=6, angle_degrees=45)\r\n \"\"\"\r\n angle_rad = math.radians(angle_degrees)\r\n obj_center = obj.location\r\n \r\n cam_x = obj_center.x + distance * math.sin(angle_rad)\r\n cam_y = obj_center.y - distance * math.cos(angle_rad)\r\n cam_z = obj_center.z + height_offset\r\n \r\n cam, target = setup_camera(\r\n location=(cam_x, cam_y, cam_z),\r\n target=tuple(obj_center),\r\n focal_length=focal_length,\r\n name=camera_name\r\n )\r\n \r\n return cam, target\r\n\r\n\r\ndef setup_turntable_camera(\r\n target=(0, 0, 0),\r\n radius=8.0,\r\n height=4.0,\r\n frames=120,\r\n focal_length=85,\r\n name=\"TurntableCamera\"\r\n):\r\n \"\"\"\r\n Create a camera that orbits around a target for turntable animations.\r\n \r\n The camera follows a circular path around the target at a fixed\r\n height and distance. Ideal for product showcase renders.\r\n \r\n Args:\r\n target: Center point to orbit around\r\n radius: Orbit radius\r\n height: Camera height above target\r\n frames: Number of frames for full rotation\r\n focal_length: Lens focal length (85mm recommended for products)\r\n \"\"\"\r\n tx, ty, tz = target\r\n \r\n # Create circular path\r\n bpy.ops.curve.primitive_bezier_circle_add(radius=radius, location=(tx, ty, tz + height))\r\n path = bpy.context.active_object\r\n path.name = f\"{name}_Path\"\r\n \r\n # Create camera\r\n cam_data = bpy.data.cameras.new(name=name)\r\n cam_data.lens = focal_length\r\n cam_obj = bpy.data.objects.new(name=name, object_data=cam_data)\r\n bpy.context.collection.objects.link(cam_obj)\r\n \r\n # Follow path constraint\r\n follow = cam_obj.constraints.new(type='FOLLOW_PATH')\r\n follow.target = path\r\n follow.use_curve_follow = True\r\n \r\n # Track To target\r\n target_empty = bpy.data.objects.new(f\"{name}_Target\", None)\r\n bpy.context.collection.objects.link(target_empty)\r\n target_empty.location = target\r\n \r\n track = cam_obj.constraints.new(type='TRACK_TO')\r\n track.target = target_empty\r\n track.track_axis = 'TRACK_NEGATIVE_Z'\r\n track.up_axis = 'UP_Y'\r\n \r\n # Animate the path\r\n bpy.context.scene.frame_start = 1\r\n bpy.context.scene.frame_end = frames\r\n path.data.path_duration = frames\r\n \r\n # Set as active camera\r\n bpy.context.scene.camera = cam_obj\r\n \r\n return cam_obj, path, target_empty\r\n\r\n\r\n# =============================================================================\r\n# RENDER SETTINGS PRESETS\r\n# =============================================================================\r\n\r\ndef setup_render_settings(\r\n resolution_x=1920,\r\n resolution_y=1080,\r\n engine='BLENDER_EEVEE',\r\n samples=64,\r\n transparent_background=False\r\n):\r\n \"\"\"\r\n Configure render settings for the scene.\r\n \r\n Args:\r\n resolution_x: Output width in pixels\r\n resolution_y: Output height in pixels\r\n engine: 'BLENDER_EEVEE' or 'CYCLES'\r\n samples: Number of render samples (higher = cleaner but slower)\r\n transparent_background: If True, renders with alpha transparent background\r\n \r\n Common resolutions:\r\n 1080p: 1920 x 1080 (standard HD)\r\n 1440p: 2560 x 1440 (QHD)\r\n 4K: 3840 x 2160 (Ultra HD)\r\n Square: 1080 x 1080 (Instagram)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.render.engine = engine\r\n scene.render.resolution_x = resolution_x\r\n scene.render.resolution_y = resolution_y\r\n scene.render.resolution_percentage = 100\r\n \r\n if engine == 'CYCLES':\r\n scene.cycles.samples = samples\r\n scene.cycles.use_denoising = True\r\n elif engine == 'BLENDER_EEVEE':\r\n scene.eevee.taa_render_samples = samples\r\n \r\n if transparent_background:\r\n scene.render.film_transparent = True\r\n \r\n return scene\r\n\r\n\r\n# =============================================================================\r\n# USAGE EXAMPLES\r\n# =============================================================================\r\n\r\n# --- Example: Product visualization setup ---\r\n# cam, target = setup_camera(\r\n# location=(5, -5, 3),\r\n# target=(0, 0, 0.5),\r\n# focal_length=85\r\n# )\r\n# setup_depth_of_field(cam, focus_distance=7.0, f_stop=2.0)\r\n# setup_render_settings(resolution_x=2560, resolution_y=1440, engine='BLENDER_EEVEE')\r\n\r\n# --- Example: Architectural interior ---\r\n# cam, target = setup_camera(\r\n# location=(3, -1, 1.7), # Eye-level height\r\n# target=(0, 5, 1.5), # Looking into room\r\n# focal_length=24 # Wide angle for interiors\r\n# )\r\n\r\n# --- Example: Dramatic low-angle hero shot ---\r\n# cam, target = setup_camera(\r\n# location=(4, -3, 0.5), # Low angle\r\n# target=(0, 0, 2), # Looking up at subject\r\n# focal_length=35\r\n# )","category":"utility","source_file":"camera_composition.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Camera Utilities. Functions for creating, positioning, and configuring cameras in Blender.","output":"import bpy\r\nimport math\r\nfrom mathutils import Vector\r\n\r\n\r\ndef add_camera(\r\n location: tuple = (7, -7, 5),\r\n rotation: tuple = (63, 0, 45),\r\n lens: float = 50,\r\n name: str = \"Camera\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add a camera to the scene.\r\n \r\n Args:\r\n location: XYZ position tuple\r\n rotation: XYZ rotation in degrees\r\n lens: Focal length in mm\r\n name: Camera name\r\n \r\n Returns:\r\n The created camera object\r\n \r\n Example:\r\n >>> cam = add_camera((10, -10, 8), (60, 0, 45), lens=85)\r\n \"\"\"\r\n bpy.ops.object.camera_add(location=location)\r\n cam = bpy.context.active_object\r\n cam.name = name\r\n cam.rotation_euler = tuple(math.radians(r) for r in rotation)\r\n cam.data.lens = lens\r\n return cam\r\n\r\n\r\ndef set_active_camera(camera: bpy.types.Object) -> None:\r\n \"\"\"\r\n Set a camera as the active render camera.\r\n \r\n Args:\r\n camera: Camera object to make active\r\n \r\n Example:\r\n >>> set_active_camera(bpy.data.objects['Camera.001'])\r\n \"\"\"\r\n bpy.context.scene.camera = camera\r\n\r\n\r\ndef point_camera_at(camera: bpy.types.Object, target: tuple) -> None:\r\n \"\"\"\r\n Point a camera to look at a specific location.\r\n \r\n Args:\r\n camera: Camera object\r\n target: XYZ location to look at\r\n \r\n Example:\r\n >>> point_camera_at(cam, (0, 0, 1))\r\n \"\"\"\r\n direction = Vector(target) - camera.location\r\n rot_quat = direction.to_track_quat('-Z', 'Y')\r\n camera.rotation_euler = rot_quat.to_euler()\r\n\r\n\r\ndef add_camera_track_to(\r\n camera: bpy.types.Object,\r\n target: bpy.types.Object,\r\n track_axis: str = 'TRACK_NEGATIVE_Z',\r\n up_axis: str = 'UP_Y'\r\n) -> bpy.types.Constraint:\r\n \"\"\"\r\n Add a Track To constraint so camera always looks at target.\r\n \r\n Args:\r\n camera: Camera object\r\n target: Object to track\r\n track_axis: Axis pointing at target\r\n up_axis: Axis pointing up\r\n \r\n Returns:\r\n The created constraint\r\n \r\n Example:\r\n >>> constraint = add_camera_track_to(cam, bpy.data.objects['Cube'])\r\n \"\"\"\r\n constraint = camera.constraints.new('TRACK_TO')\r\n constraint.target = target\r\n constraint.track_axis = track_axis\r\n constraint.up_axis = up_axis\r\n return constraint\r\n\r\n\r\ndef setup_depth_of_field(\r\n camera: bpy.types.Object,\r\n focus_object: bpy.types.Object = None,\r\n focus_distance: float = None,\r\n fstop: float = 2.8,\r\n blades: int = 0\r\n) -> None:\r\n \"\"\"\r\n Configure depth of field settings for cinematic blur.\r\n \r\n Args:\r\n camera: Camera object\r\n focus_object: Object to focus on (overrides distance)\r\n focus_distance: Manual focus distance in meters\r\n fstop: Aperture f-stop (lower = more blur)\r\n blades: Aperture blade count (0 = circular)\r\n \r\n Example:\r\n >>> setup_depth_of_field(cam, focus_object=subject, fstop=1.4)\r\n \"\"\"\r\n camera.data.dof.use_dof = True\r\n camera.data.dof.aperture_fstop = fstop\r\n camera.data.dof.aperture_blades = blades\r\n \r\n if focus_object:\r\n camera.data.dof.focus_object = focus_object\r\n elif focus_distance:\r\n camera.data.dof.focus_distance = focus_distance\r\n\r\n\r\ndef setup_render_resolution(\r\n width: int = 1920,\r\n height: int = 1080,\r\n percentage: int = 100\r\n) -> None:\r\n \"\"\"\r\n Set render resolution.\r\n \r\n Args:\r\n width: Horizontal resolution in pixels\r\n height: Vertical resolution in pixels\r\n percentage: Scale factor (50 = half resolution)\r\n \r\n Example:\r\n >>> setup_render_resolution(3840, 2160, 100) # 4K\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.render.resolution_x = width\r\n scene.render.resolution_y = height\r\n scene.render.resolution_percentage = percentage\r\n\r\n\r\ndef create_camera_orbit_path(\r\n center: tuple = (0, 0, 0),\r\n radius: float = 10,\r\n height: float = 5,\r\n name: str = \"CameraPath\"\r\n) -> tuple:\r\n \"\"\"\r\n Create a circular path for camera animation.\r\n \r\n Args:\r\n center: Center point of orbit\r\n radius: Distance from center\r\n height: Height above center\r\n name: Path object name\r\n \r\n Returns:\r\n Tuple of (path_object, camera_object)\r\n \r\n Example:\r\n >>> path, cam = create_camera_orbit_path(radius=8, height=4)\r\n \"\"\"\r\n # Create circle path\r\n bpy.ops.curve.primitive_bezier_circle_add(radius=radius, location=(center[0], center[1], center[2] + height))\r\n path = bpy.context.active_object\r\n path.name = name\r\n \r\n # Create camera\r\n cam_location = (center[0] + radius, center[1], center[2] + height)\r\n bpy.ops.object.camera_add(location=cam_location)\r\n cam = bpy.context.active_object\r\n cam.name = f\"{name}_Camera\"\r\n \r\n # Add follow path constraint\r\n constraint = cam.constraints.new('FOLLOW_PATH')\r\n constraint.target = path\r\n constraint.use_curve_follow = True\r\n \r\n # Point camera at center\r\n point_camera_at(cam, center)\r\n \r\n return path, cam","category":"utility","source_file":"camera_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Set Active Camera. Category: utility","output":"import bpy\n\ndef set_active_camera(camera: bpy.types.Object) -> None:\r\n \"\"\"\r\n Set a camera as the active render camera.\r\n \r\n Args:\r\n camera: Camera object to make active\r\n \r\n Example:\r\n >>> set_active_camera(bpy.data.objects['Camera.001'])\r\n \"\"\"\r\n bpy.context.scene.camera = camera\r\n\r\n\r\ndef point_camera_at(camera: bpy.types.Object, target: tuple) -> None:\r\n \"\"\"\r\n Point a camera to look at a specific location.\r\n \r\n Args:\r\n camera: Camera object\r\n target: XYZ location to look at\r\n \r\n Example:\r\n >>> point_camera_at(cam, (0, 0, 1))\r\n \"\"\"\r\n direction = Vector(target) - camera.location\r\n rot_quat = direction.to_track_quat('-Z', 'Y')\r\n camera.rotation_euler = rot_quat.to_euler()\r\n\r\n\r\ndef add_camera_track_to(\r\n camera: bpy.types.Object,\r\n target: bpy.types.Object,\r\n track_axis: str = 'TRACK_NEGATIVE_Z',\r\n up_axis: str = 'UP_Y'\r\n) -> bpy.types.Constraint:\r\n \"\"\"\r\n Add a Track To constraint so camera always looks at target.\r\n \r\n Args:\r\n camera: Camera object\r\n target: Object to track\r\n track_axis: Axis pointing at target\r\n up_axis: Axis pointing up\r\n \r\n Returns:\r\n The created constraint\r\n \r\n Example:\r\n >>> constraint = add_camera_track_to(cam, bpy.data.objects['Cube'])\r\n \"\"\"\r\n constraint = camera.constraints.new('TRACK_TO')\r\n constraint.target = target\r\n constraint.track_axis = track_axis\r\n constraint.up_axis = up_axis\r\n return constraint\r\n\r\n\r\ndef setup_depth_of_field(\r\n camera: bpy.types.Object,\r\n focus_object: bpy.types.Object = None,\r\n focus_distance: float = None,\r\n fstop: float = 2.8,\r\n blades: int = 0\r\n) -> None:\r\n \"\"\"\r\n Configure depth of field settings for cinematic blur.\r\n \r\n Args:\r\n camera: Camera object\r\n focus_object: Object to focus on (overrides distance)\r\n focus_distance: Manual focus distance in meters\r\n fstop: Aperture f-stop (lower = more blur)\r\n blades: Aperture blade count (0 = circular)\r\n \r\n Example:\r\n >>> setup_depth_of_field(cam, focus_object=subject, fstop=1.4)\r\n \"\"\"\r\n camera.data.dof.use_dof = True\r\n camera.data.dof.aperture_fstop = fstop\r\n camera.data.dof.aperture_blades = blades\r\n \r\n if focus_object:\r\n camera.data.dof.focus_object = focus_object\r\n elif focus_distance:\r\n camera.data.dof.focus_distance = focus_distance\r\n\r\n\r\ndef setup_render_resolution(\r\n width: int = 1920,\r\n height: int = 1080,\r\n percentage: int = 100\r\n) -> None:\r\n \"\"\"\r\n Set render resolution.\r\n \r\n Args:\r\n width: Horizontal resolution in pixels\r\n height: Vertical resolution in pixels\r\n percentage: Scale factor (50 = half resolution)\r\n \r\n Example:\r\n >>> setup_render_resolution(3840, 2160, 100) # 4K\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.render.resolution_x = width\r\n scene.render.resolution_y = height\r\n scene.render.resolution_percentage = percentage\r\n\r\n\r\ndef create_camera_orbit_path(\r\n center: tuple = (0, 0, 0),\r\n radius: float = 10,\r\n height: float = 5,\r\n name: str = \"CameraPath\"\r\n) -> tuple:\r\n \"\"\"\r\n Create a circular path for camera animation.\r\n \r\n Args:\r\n center: Center point of orbit\r\n radius: Distance from center\r\n height: Height above center\r\n name: Path object name\r\n \r\n Returns:\r\n Tuple of (path_object, camera_object)\r\n \r\n Example:\r\n >>> path, cam = create_camera_orbit_path(radius=8, height=4)\r\n \"\"\"\r\n # Create circle path\r\n bpy.ops.curve.primitive_bezier_circle_add(radius=radius, location=(center[0], center[1], center[2] + height))\r\n path = bpy.context.active_object\r\n path.name = name\r\n \r\n # Create camera\r\n cam_location = (center[0] + radius, center[1], center[2] + height)\r\n bpy.ops.object.camera_add(location=cam_location)\r\n cam = bpy.context.active_object\r\n cam.name = f\"{name}_Camera\"\r\n \r\n # Add follow path constraint\r\n constraint = cam.constraints.new('FOLLOW_PATH')\r\n constraint.target = path\r\n constraint.use_curve_follow = True\r\n \r\n # Point camera at center\r\n point_camera_at(cam, center)\r\n \r\n return path, cam","category":"utility","source_file":"camera_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Scene Cleanup Utilities. Functions to clear the scene or remove unused data blocks.","output":"import bpy\r\n\r\ndef delete_all_objects():\r\n if bpy.context.active_object and bpy.context.active_object.mode == 'EDIT':\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete(use_global=False)\r\n\r\ndef delete_object(obj):\r\n bpy.data.objects.remove(obj, do_unlink=True)\r\n\r\ndef purge_orphans():\r\n # Purge unused data blocks (meshes, materials, textures, etc.)\r\n for block in bpy.data.meshes:\r\n if block.users == 0:\r\n bpy.data.meshes.remove(block)\r\n \r\n for block in bpy.data.materials:\r\n if block.users == 0:\r\n bpy.data.materials.remove(block)\r\n \r\n for block in bpy.data.textures:\r\n if block.users == 0:\r\n bpy.data.textures.remove(block)\r\n \r\n for block in bpy.data.images:\r\n if block.users == 0:\r\n bpy.data.images.remove(block)","category":"utility","source_file":"clean_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Delete All Objects. Category: utility","output":"import bpy\n\ndef delete_all_objects():\n if bpy.context.active_object and bpy.context.active_object.mode == 'EDIT':\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete(use_global=False)","category":"utility","source_file":"clean_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Delete Object. Category: utility","output":"import bpy\n\ndef delete_object(obj):\n bpy.data.objects.remove(obj, do_unlink=True)","category":"utility","source_file":"clean_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Purge Orphans. Category: utility","output":"import bpy\n\ndef purge_orphans():\n # Purge unused data blocks (meshes, materials, textures, etc.)\r\n for block in bpy.data.meshes:\r\n if block.users == 0:\r\n bpy.data.meshes.remove(block)\r\n \r\n for block in bpy.data.materials:\r\n if block.users == 0:\r\n bpy.data.materials.remove(block)\r\n \r\n for block in bpy.data.textures:\r\n if block.users == 0:\r\n bpy.data.textures.remove(block)\r\n \r\n for block in bpy.data.images:\r\n if block.users == 0:\r\n bpy.data.images.remove(block)","category":"utility","source_file":"clean_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Collection Management Utilities. Functions for organizing objects into collections.","output":"import bpy\r\n\r\n\r\ndef create_collection(\r\n name: str,\r\n parent: bpy.types.Collection = None\r\n) -> bpy.types.Collection:\r\n \"\"\"\r\n Create a new collection.\r\n \r\n Args:\r\n name: Collection name\r\n parent: Parent collection (uses scene collection if None)\r\n \r\n Returns:\r\n The created collection\r\n \"\"\"\r\n collection = bpy.data.collections.new(name)\r\n \r\n if parent:\r\n parent.children.link(collection)\r\n else:\r\n bpy.context.scene.collection.children.link(collection)\r\n \r\n return collection\r\n\r\n\r\ndef move_to_collection(\r\n obj: bpy.types.Object,\r\n collection: bpy.types.Collection,\r\n unlink_others: bool = True\r\n) -> None:\r\n \"\"\"Move object to collection.\"\"\"\r\n if unlink_others:\r\n for coll in obj.users_collection:\r\n coll.objects.unlink(obj)\r\n \r\n if obj.name not in collection.objects:\r\n collection.objects.link(obj)\r\n\r\n\r\ndef move_selected_to_collection(\r\n collection: bpy.types.Collection,\r\n unlink_others: bool = True\r\n) -> None:\r\n \"\"\"Move all selected objects to collection.\"\"\"\r\n for obj in bpy.context.selected_objects:\r\n move_to_collection(obj, collection, unlink_others)\r\n\r\n\r\ndef get_or_create_collection(name: str) -> bpy.types.Collection:\r\n \"\"\"Get existing or create new collection.\"\"\"\r\n if name in bpy.data.collections:\r\n return bpy.data.collections[name]\r\n return create_collection(name)\r\n\r\n\r\ndef set_collection_visibility(\r\n collection: bpy.types.Collection,\r\n visible: bool = True,\r\n viewport: bool = True,\r\n render: bool = True\r\n) -> None:\r\n \"\"\"Set collection visibility.\"\"\"\r\n # Find layer collection\r\n def find_layer_collection(layer_coll, name):\r\n if layer_coll.name == name:\r\n return layer_coll\r\n for child in layer_coll.children:\r\n result = find_layer_collection(child, name)\r\n if result:\r\n return result\r\n return None\r\n \r\n layer_coll = find_layer_collection(\r\n bpy.context.view_layer.layer_collection,\r\n collection.name\r\n )\r\n \r\n if layer_coll:\r\n layer_coll.exclude = not visible\r\n \r\n collection.hide_viewport = not viewport\r\n collection.hide_render = not render\r\n\r\n\r\ndef delete_collection(\r\n collection: bpy.types.Collection,\r\n delete_objects: bool = False\r\n) -> None:\r\n \"\"\"Delete collection.\"\"\"\r\n if delete_objects:\r\n for obj in collection.objects[:]:\r\n bpy.data.objects.remove(obj)\r\n \r\n bpy.data.collections.remove(collection)\r\n\r\n\r\ndef organize_by_type() -> dict:\r\n \"\"\"Organize scene objects by type into collections.\"\"\"\r\n collections = {}\r\n \r\n type_names = {\r\n 'MESH': 'Meshes',\r\n 'CURVE': 'Curves',\r\n 'LIGHT': 'Lights',\r\n 'CAMERA': 'Cameras',\r\n 'ARMATURE': 'Armatures',\r\n 'EMPTY': 'Empties'\r\n }\r\n \r\n for obj in bpy.context.scene.objects:\r\n type_name = type_names.get(obj.type, 'Other')\r\n \r\n if type_name not in collections:\r\n collections[type_name] = get_or_create_collection(type_name)\r\n \r\n move_to_collection(obj, collections[type_name])\r\n \r\n return collections\r\n\r\n\r\ndef select_collection_objects(collection: bpy.types.Collection) -> None:\r\n \"\"\"Select all objects in collection.\"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n for obj in collection.objects:\r\n obj.select_set(True)\r\n\r\n\r\ndef get_collection_bounds(collection: bpy.types.Collection) -> tuple:\r\n \"\"\"Get bounding box of all objects in collection.\"\"\"\r\n min_co = [float('inf')] * 3\r\n max_co = [float('-inf')] * 3\r\n \r\n for obj in collection.objects:\r\n if obj.type == 'MESH':\r\n for corner in obj.bound_box:\r\n world_co = obj.matrix_world @ corner\r\n for i in range(3):\r\n min_co[i] = min(min_co[i], world_co[i])\r\n max_co[i] = max(max_co[i], world_co[i])\r\n \r\n return tuple(min_co), tuple(max_co)","category":"utility","source_file":"collection_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Get Or Create Collection. Category: utility","output":"import bpy\n\ndef get_or_create_collection(name: str) -> bpy.types.Collection:\r\n \"\"\"Get existing or create new collection.\"\"\"\r\n if name in bpy.data.collections:\r\n return bpy.data.collections[name]\r\n return create_collection(name)\r\n\r\n\r\ndef set_collection_visibility(\r\n collection: bpy.types.Collection,\r\n visible: bool = True,\r\n viewport: bool = True,\r\n render: bool = True\r\n) -> None:\r\n \"\"\"Set collection visibility.\"\"\"\r\n # Find layer collection\r\n def find_layer_collection(layer_coll, name):\n if layer_coll.name == name:\r\n return layer_coll\r\n for child in layer_coll.children:\r\n result = find_layer_collection(child, name)\r\n if result:\r\n return result\r\n return None\r\n \r\n layer_coll = find_layer_collection(\r\n bpy.context.view_layer.layer_collection,\r\n collection.name\r\n )\r\n \r\n if layer_coll:\r\n layer_coll.exclude = not visible\r\n \r\n collection.hide_viewport = not viewport\r\n collection.hide_render = not render","category":"utility","source_file":"collection_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Organize By Type. Category: utility","output":"import bpy\n\ndef organize_by_type() -> dict:\r\n \"\"\"Organize scene objects by type into collections.\"\"\"\r\n collections = {}\r\n \r\n type_names = {\r\n 'MESH': 'Meshes',\r\n 'CURVE': 'Curves',\r\n 'LIGHT': 'Lights',\r\n 'CAMERA': 'Cameras',\r\n 'ARMATURE': 'Armatures',\r\n 'EMPTY': 'Empties'\r\n }\r\n \r\n for obj in bpy.context.scene.objects:\r\n type_name = type_names.get(obj.type, 'Other')\r\n \r\n if type_name not in collections:\r\n collections[type_name] = get_or_create_collection(type_name)\r\n \r\n move_to_collection(obj, collections[type_name])\r\n \r\n return collections\r\n\r\n\r\ndef select_collection_objects(collection: bpy.types.Collection) -> None:\r\n \"\"\"Select all objects in collection.\"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n for obj in collection.objects:\r\n obj.select_set(True)\r\n\r\n\r\ndef get_collection_bounds(collection: bpy.types.Collection) -> tuple:\r\n \"\"\"Get bounding box of all objects in collection.\"\"\"\r\n min_co = [float('inf')] * 3\r\n max_co = [float('-inf')] * 3\r\n \r\n for obj in collection.objects:\r\n if obj.type == 'MESH':\r\n for corner in obj.bound_box:\r\n world_co = obj.matrix_world @ corner\r\n for i in range(3):\n min_co[i] = min(min_co[i], world_co[i])\r\n max_co[i] = max(max_co[i], world_co[i])\r\n \r\n return tuple(min_co), tuple(max_co)","category":"utility","source_file":"collection_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Compositing Utilities. Functions for setting up compositing node setups.","output":"import bpy\r\n\r\n\r\ndef enable_compositing() -> bpy.types.NodeTree:\r\n \"\"\"Enable compositing and return node tree.\"\"\"\r\n return bpy.context.scene.node_tree\r\n\r\n\r\ndef clear_compositing_nodes() -> None:\r\n \"\"\"Clear all compositing nodes.\"\"\"\r\n tree = enable_compositing()\r\n tree.nodes.clear()\r\n\r\n\r\ndef setup_basic_composite() -> dict:\r\n \"\"\"Set up basic render layers to composite output.\"\"\"\r\n tree = enable_compositing()\r\n tree.nodes.clear()\r\n \r\n render = tree.nodes.new('CompositorNodeRLayers')\r\n render.location = (0, 0)\r\n \r\n composite = tree.nodes.new('CompositorNodeComposite')\r\n composite.location = (400, 0)\r\n \r\n viewer = tree.nodes.new('CompositorNodeViewer')\r\n viewer.location = (400, -200)\r\n \r\n tree.links.new(render.outputs['Image'], composite.inputs['Image'])\r\n tree.links.new(render.outputs['Image'], viewer.inputs['Image'])\r\n \r\n return {'render': render, 'composite': composite, 'viewer': viewer}\r\n\r\n\r\ndef add_glare(\r\n input_socket,\r\n glare_type: str = 'FOG_GLOW',\r\n threshold: float = 1.0,\r\n size: int = 8\r\n) -> bpy.types.Node:\r\n \"\"\"Add glare/glow effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n glare = tree.nodes.new('CompositorNodeGlare')\r\n glare.glare_type = glare_type # 'GHOSTS', 'STREAKS', 'FOG_GLOW', 'SIMPLE_STAR'\r\n glare.threshold = threshold\r\n glare.size = size\r\n \r\n tree.links.new(input_socket, glare.inputs['Image'])\r\n \r\n return glare\r\n\r\n\r\ndef add_blur(\r\n input_socket,\r\n blur_type: str = 'FLAT',\r\n size_x: float = 5,\r\n size_y: float = 5\r\n) -> bpy.types.Node:\r\n \"\"\"Add blur effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n blur = tree.nodes.new('CompositorNodeBlur')\r\n blur.filter_type = blur_type # 'FLAT', 'TENT', 'QUAD', 'GAUSS'\r\n blur.size_x = size_x\r\n blur.size_y = size_y\r\n \r\n tree.links.new(input_socket, blur.inputs['Image'])\r\n \r\n return blur\r\n\r\n\r\ndef add_color_correction(\r\n input_socket,\r\n brightness: float = 0.0,\r\n contrast: float = 0.0,\r\n saturation: float = 1.0\r\n) -> bpy.types.Node:\r\n \"\"\"Add brightness/contrast adjustment.\"\"\"\r\n tree = enable_compositing()\r\n \r\n bc = tree.nodes.new('CompositorNodeBrightContrast')\r\n bc.inputs['Bright'].default_value = brightness\r\n bc.inputs['Contrast'].default_value = contrast\r\n \r\n tree.links.new(input_socket, bc.inputs['Image'])\r\n \r\n return bc\r\n\r\n\r\ndef add_vignette(input_socket, strength: float = 0.5) -> bpy.types.Node:\r\n \"\"\"Add vignette effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n # Ellipse mask\r\n mask = tree.nodes.new('CompositorNodeEllipseMask')\r\n mask.width = 1.0\r\n mask.height = 1.0\r\n \r\n # Blur the mask\r\n blur = tree.nodes.new('CompositorNodeBlur')\r\n blur.size_x = 200\r\n blur.size_y = 200\r\n \r\n tree.links.new(mask.outputs['Mask'], blur.inputs['Image'])\r\n \r\n # Mix\r\n mix = tree.nodes.new('CompositorNodeMixRGB')\r\n mix.blend_type = 'MULTIPLY'\r\n mix.inputs['Fac'].default_value = strength\r\n \r\n tree.links.new(input_socket, mix.inputs[1])\r\n tree.links.new(blur.outputs['Image'], mix.inputs[2])\r\n \r\n return mix\r\n\r\n\r\ndef add_lens_distortion(\r\n input_socket,\r\n distortion: float = 0.0,\r\n dispersion: float = 0.0\r\n) -> bpy.types.Node:\r\n \"\"\"Add lens distortion effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n lens = tree.nodes.new('CompositorNodeLensdist')\r\n lens.inputs['Distort'].default_value = distortion\r\n lens.inputs['Dispersion'].default_value = dispersion\r\n \r\n tree.links.new(input_socket, lens.inputs['Image'])\r\n \r\n return lens\r\n\r\n\r\ndef add_film_grain(input_socket, strength: float = 0.1) -> bpy.types.Node:\r\n \"\"\"Add film grain effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n # Noise texture\r\n tex = tree.nodes.new('CompositorNodeTexture')\r\n noise_tex = bpy.data.textures.new(\"GrainNoise\", 'NOISE')\r\n tex.texture = noise_tex\r\n \r\n # Mix with image\r\n mix = tree.nodes.new('CompositorNodeMixRGB')\r\n mix.blend_type = 'OVERLAY'\r\n mix.inputs['Fac'].default_value = strength\r\n \r\n tree.links.new(input_socket, mix.inputs[1])\r\n tree.links.new(tex.outputs['Color'], mix.inputs[2])\r\n \r\n return mix\r\n\r\n\r\ndef setup_depth_of_field_composite(\r\n focus_distance: float = 5.0,\r\n fstop: float = 2.8\r\n) -> dict:\r\n \"\"\"Set up depth of field in compositing.\"\"\"\r\n tree = enable_compositing()\r\n \r\n # Enable Z pass\r\n bpy.context.view_layer.use_pass_z = True\r\n \r\n nodes = setup_basic_composite()\r\n \r\n defocus = tree.nodes.new('CompositorNodeDefocus')\r\n defocus.use_zbuffer = True\r\n defocus.z_scale = 1.0\r\n defocus.f_stop = fstop\r\n \r\n tree.links.new(nodes['render'].outputs['Image'], defocus.inputs['Image'])\r\n tree.links.new(nodes['render'].outputs['Depth'], defocus.inputs['Z'])\r\n tree.links.new(defocus.outputs['Image'], nodes['composite'].inputs['Image'])\r\n \r\n nodes['defocus'] = defocus\r\n return nodes","category":"utility","source_file":"compositing_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Enable Compositing. Category: utility","output":"import bpy\n\ndef enable_compositing() -> bpy.types.NodeTree:\r\n \"\"\"Enable compositing and return node tree.\"\"\"\r\n return bpy.context.scene.node_tree\r\n\r\n\r\ndef clear_compositing_nodes() -> None:\r\n \"\"\"Clear all compositing nodes.\"\"\"\r\n tree = enable_compositing()\r\n tree.nodes.clear()\r\n\r\n\r\ndef setup_basic_composite() -> dict:\r\n \"\"\"Set up basic render layers to composite output.\"\"\"\r\n tree = enable_compositing()\r\n tree.nodes.clear()\r\n \r\n render = tree.nodes.new('CompositorNodeRLayers')\r\n render.location = (0, 0)\r\n \r\n composite = tree.nodes.new('CompositorNodeComposite')\r\n composite.location = (400, 0)\r\n \r\n viewer = tree.nodes.new('CompositorNodeViewer')\r\n viewer.location = (400, -200)\r\n \r\n tree.links.new(render.outputs['Image'], composite.inputs['Image'])\r\n tree.links.new(render.outputs['Image'], viewer.inputs['Image'])\r\n \r\n return {'render': render, 'composite': composite, 'viewer': viewer}\r\n\r\n\r\ndef add_glare(\r\n input_socket,\r\n glare_type: str = 'FOG_GLOW',\r\n threshold: float = 1.0,\r\n size: int = 8\r\n) -> bpy.types.Node:\r\n \"\"\"Add glare/glow effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n glare = tree.nodes.new('CompositorNodeGlare')\r\n glare.glare_type = glare_type # 'GHOSTS', 'STREAKS', 'FOG_GLOW', 'SIMPLE_STAR'\r\n glare.threshold = threshold\r\n glare.size = size\r\n \r\n tree.links.new(input_socket, glare.inputs['Image'])\r\n \r\n return glare\r\n\r\n\r\ndef add_blur(\r\n input_socket,\r\n blur_type: str = 'FLAT',\r\n size_x: float = 5,\r\n size_y: float = 5\r\n) -> bpy.types.Node:\r\n \"\"\"Add blur effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n blur = tree.nodes.new('CompositorNodeBlur')\r\n blur.filter_type = blur_type # 'FLAT', 'TENT', 'QUAD', 'GAUSS'\r\n blur.size_x = size_x\r\n blur.size_y = size_y\r\n \r\n tree.links.new(input_socket, blur.inputs['Image'])\r\n \r\n return blur\r\n\r\n\r\ndef add_color_correction(\r\n input_socket,\r\n brightness: float = 0.0,\r\n contrast: float = 0.0,\r\n saturation: float = 1.0\r\n) -> bpy.types.Node:\r\n \"\"\"Add brightness/contrast adjustment.\"\"\"\r\n tree = enable_compositing()\r\n \r\n bc = tree.nodes.new('CompositorNodeBrightContrast')\r\n bc.inputs['Bright'].default_value = brightness\r\n bc.inputs['Contrast'].default_value = contrast\r\n \r\n tree.links.new(input_socket, bc.inputs['Image'])\r\n \r\n return bc\r\n\r\n\r\ndef add_vignette(input_socket, strength: float = 0.5) -> bpy.types.Node:\r\n \"\"\"Add vignette effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n # Ellipse mask\r\n mask = tree.nodes.new('CompositorNodeEllipseMask')\r\n mask.width = 1.0\r\n mask.height = 1.0\r\n \r\n # Blur the mask\r\n blur = tree.nodes.new('CompositorNodeBlur')\r\n blur.size_x = 200\r\n blur.size_y = 200\r\n \r\n tree.links.new(mask.outputs['Mask'], blur.inputs['Image'])\r\n \r\n # Mix\r\n mix = tree.nodes.new('CompositorNodeMixRGB')\r\n mix.blend_type = 'MULTIPLY'\r\n mix.inputs['Fac'].default_value = strength\r\n \r\n tree.links.new(input_socket, mix.inputs[1])\r\n tree.links.new(blur.outputs['Image'], mix.inputs[2])\r\n \r\n return mix\r\n\r\n\r\ndef add_lens_distortion(\r\n input_socket,\r\n distortion: float = 0.0,\r\n dispersion: float = 0.0\r\n) -> bpy.types.Node:\r\n \"\"\"Add lens distortion effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n lens = tree.nodes.new('CompositorNodeLensdist')\r\n lens.inputs['Distort'].default_value = distortion\r\n lens.inputs['Dispersion'].default_value = dispersion\r\n \r\n tree.links.new(input_socket, lens.inputs['Image'])\r\n \r\n return lens\r\n\r\n\r\ndef add_film_grain(input_socket, strength: float = 0.1) -> bpy.types.Node:\r\n \"\"\"Add film grain effect.\"\"\"\r\n tree = enable_compositing()\r\n \r\n # Noise texture\r\n tex = tree.nodes.new('CompositorNodeTexture')\r\n noise_tex = bpy.data.textures.new(\"GrainNoise\", 'NOISE')\r\n tex.texture = noise_tex\r\n \r\n # Mix with image\r\n mix = tree.nodes.new('CompositorNodeMixRGB')\r\n mix.blend_type = 'OVERLAY'\r\n mix.inputs['Fac'].default_value = strength\r\n \r\n tree.links.new(input_socket, mix.inputs[1])\r\n tree.links.new(tex.outputs['Color'], mix.inputs[2])\r\n \r\n return mix\r\n\r\n\r\ndef setup_depth_of_field_composite(\r\n focus_distance: float = 5.0,\r\n fstop: float = 2.8\r\n) -> dict:\r\n \"\"\"Set up depth of field in compositing.\"\"\"\r\n tree = enable_compositing()\r\n \r\n # Enable Z pass\r\n bpy.context.view_layer.use_pass_z = True\r\n \r\n nodes = setup_basic_composite()\r\n \r\n defocus = tree.nodes.new('CompositorNodeDefocus')\r\n defocus.use_zbuffer = True\r\n defocus.z_scale = 1.0\r\n defocus.f_stop = fstop\r\n \r\n tree.links.new(nodes['render'].outputs['Image'], defocus.inputs['Image'])\r\n tree.links.new(nodes['render'].outputs['Depth'], defocus.inputs['Z'])\r\n tree.links.new(defocus.outputs['Image'], nodes['composite'].inputs['Image'])\r\n \r\n nodes['defocus'] = defocus\r\n return nodes","category":"utility","source_file":"compositing_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Constraint Utilities. Functions for adding and configuring object and bone constraints.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef add_copy_location(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n use_offset: bool = False,\r\n axes: str = 'XYZ'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add copy location constraint.\"\"\"\r\n c = obj.constraints.new('COPY_LOCATION')\r\n c.target = target\r\n c.use_offset = use_offset\r\n c.use_x = 'X' in axes\r\n c.use_y = 'Y' in axes\r\n c.use_z = 'Z' in axes\r\n return c\r\n\r\n\r\ndef add_copy_rotation(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n use_offset: bool = False,\r\n axes: str = 'XYZ'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add copy rotation constraint.\"\"\"\r\n c = obj.constraints.new('COPY_ROTATION')\r\n c.target = target\r\n c.use_offset = use_offset\r\n c.use_x = 'X' in axes\r\n c.use_y = 'Y' in axes\r\n c.use_z = 'Z' in axes\r\n return c\r\n\r\n\r\ndef add_copy_scale(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n axes: str = 'XYZ'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add copy scale constraint.\"\"\"\r\n c = obj.constraints.new('COPY_SCALE')\r\n c.target = target\r\n c.use_x = 'X' in axes\r\n c.use_y = 'Y' in axes\r\n c.use_z = 'Z' in axes\r\n return c\r\n\r\n\r\ndef add_copy_transforms(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n mix_mode: str = 'REPLACE'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add copy transforms constraint.\"\"\"\r\n c = obj.constraints.new('COPY_TRANSFORMS')\r\n c.target = target\r\n c.mix_mode = mix_mode\r\n return c\r\n\r\n\r\ndef add_track_to(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n track_axis: str = 'TRACK_NEGATIVE_Z',\r\n up_axis: str = 'UP_Y'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add track to constraint (point at target).\"\"\"\r\n c = obj.constraints.new('TRACK_TO')\r\n c.target = target\r\n c.track_axis = track_axis\r\n c.up_axis = up_axis\r\n return c\r\n\r\n\r\ndef add_damped_track(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n track_axis: str = 'TRACK_NEGATIVE_Z'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add damped track constraint (simpler pointing).\"\"\"\r\n c = obj.constraints.new('DAMPED_TRACK')\r\n c.target = target\r\n c.track_axis = track_axis\r\n return c\r\n\r\n\r\ndef add_follow_path(\r\n obj: bpy.types.Object,\r\n path: bpy.types.Object,\r\n use_curve_follow: bool = True,\r\n forward_axis: str = 'FORWARD_Y',\r\n up_axis: str = 'UP_Z'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add follow path constraint.\"\"\"\r\n c = obj.constraints.new('FOLLOW_PATH')\r\n c.target = path\r\n c.use_curve_follow = use_curve_follow\r\n c.forward_axis = forward_axis\r\n c.up_axis = up_axis\r\n return c\r\n\r\n\r\ndef add_limit_location(\r\n obj: bpy.types.Object,\r\n min_x: float = None, max_x: float = None,\r\n min_y: float = None, max_y: float = None,\r\n min_z: float = None, max_z: float = None,\r\n use_transform_limit: bool = True\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add limit location constraint.\"\"\"\r\n c = obj.constraints.new('LIMIT_LOCATION')\r\n \r\n if min_x is not None:\r\n c.use_min_x = True\r\n c.min_x = min_x\r\n if max_x is not None:\r\n c.use_max_x = True\r\n c.max_x = max_x\r\n if min_y is not None:\r\n c.use_min_y = True\r\n c.min_y = min_y\r\n if max_y is not None:\r\n c.use_max_y = True\r\n c.max_y = max_y\r\n if min_z is not None:\r\n c.use_min_z = True\r\n c.min_z = min_z\r\n if max_z is not None:\r\n c.use_max_z = True\r\n c.max_z = max_z\r\n \r\n c.use_transform_limit = use_transform_limit\r\n return c\r\n\r\n\r\ndef add_limit_rotation(\r\n obj: bpy.types.Object,\r\n min_x: float = None, max_x: float = None,\r\n min_y: float = None, max_y: float = None,\r\n min_z: float = None, max_z: float = None\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add limit rotation constraint (angles in degrees).\"\"\"\r\n c = obj.constraints.new('LIMIT_ROTATION')\r\n \r\n if min_x is not None:\r\n c.use_limit_x = True\r\n c.min_x = math.radians(min_x)\r\n if max_x is not None:\r\n c.use_limit_x = True\r\n c.max_x = math.radians(max_x)\r\n if min_y is not None:\r\n c.use_limit_y = True\r\n c.min_y = math.radians(min_y)\r\n if max_y is not None:\r\n c.use_limit_y = True\r\n c.max_y = math.radians(max_y)\r\n if min_z is not None:\r\n c.use_limit_z = True\r\n c.min_z = math.radians(min_z)\r\n if max_z is not None:\r\n c.use_limit_z = True\r\n c.max_z = math.radians(max_z)\r\n \r\n return c\r\n\r\n\r\ndef add_floor(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n offset: float = 0.0,\r\n floor_location: str = 'FLOOR_Z'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add floor constraint (prevent going below target).\"\"\"\r\n c = obj.constraints.new('FLOOR')\r\n c.target = target\r\n c.offset = offset\r\n c.floor_location = floor_location\r\n return c\r\n\r\n\r\ndef add_child_of(\r\n obj: bpy.types.Object,\r\n parent: bpy.types.Object,\r\n subtarget: str = None\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add child of constraint.\"\"\"\r\n c = obj.constraints.new('CHILD_OF')\r\n c.target = parent\r\n if subtarget:\r\n c.subtarget = subtarget\r\n return c\r\n\r\n\r\ndef add_locked_track(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n track_axis: str = 'TRACK_Y',\r\n lock_axis: str = 'LOCK_Z'\r\n) -> bpy.types.Constraint:\r\n \"\"\"Add locked track constraint.\"\"\"\r\n c = obj.constraints.new('LOCKED_TRACK')\r\n c.target = target\r\n c.track_axis = track_axis\r\n c.lock_axis = lock_axis\r\n return c\r\n\r\n\r\ndef remove_all_constraints(obj: bpy.types.Object) -> None:\r\n \"\"\"Remove all constraints from object.\"\"\"\r\n for c in obj.constraints[:]:\r\n obj.constraints.remove(c)\r\n\r\n\r\ndef set_constraint_influence(\r\n obj: bpy.types.Object,\r\n constraint_name: str,\r\n influence: float\r\n) -> None:\r\n \"\"\"Set constraint influence (0-1).\"\"\"\r\n if constraint_name in obj.constraints:\r\n obj.constraints[constraint_name].influence = influence","category":"utility","source_file":"constraint_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Remove All Constraints. Category: utility","output":"import bpy\n\ndef remove_all_constraints(obj: bpy.types.Object) -> None:\r\n \"\"\"Remove all constraints from object.\"\"\"\r\n for c in obj.constraints[:]:\r\n obj.constraints.remove(c)\r\n\r\n\r\ndef set_constraint_influence(\r\n obj: bpy.types.Object,\r\n constraint_name: str,\r\n influence: float\r\n) -> None:\r\n \"\"\"Set constraint influence (0-1).\"\"\"\r\n if constraint_name in obj.constraints:\r\n obj.constraints[constraint_name].influence = influence","category":"utility","source_file":"constraint_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Curve and Path Utilities. Functions for creating and manipulating curves, paths, and splines.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_bezier_curve(\r\n points: list,\r\n cyclic: bool = False,\r\n resolution: int = 12,\r\n name: str = \"BezierCurve\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a bezier curve from points.\r\n \r\n Args:\r\n points: List of (x, y, z) tuples\r\n cyclic: Close the curve\r\n resolution: Curve smoothness\r\n name: Object name\r\n \r\n Returns:\r\n The created curve object\r\n \r\n Example:\r\n >>> pts = [(0,0,0), (1,0,1), (2,0,0), (3,0,1)]\r\n >>> curve = create_bezier_curve(pts)\r\n \"\"\"\r\n curve_data = bpy.data.curves.new(name, type='CURVE')\r\n curve_data.dimensions = '3D'\r\n curve_data.resolution_u = resolution\r\n \r\n spline = curve_data.splines.new('BEZIER')\r\n spline.bezier_points.add(len(points) - 1)\r\n \r\n for i, point in enumerate(points):\r\n bp = spline.bezier_points[i]\r\n bp.co = point\r\n bp.handle_type_left = 'AUTO'\r\n bp.handle_type_right = 'AUTO'\r\n \r\n spline.use_cyclic_u = cyclic\r\n \r\n curve_obj = bpy.data.objects.new(name, curve_data)\r\n bpy.context.collection.objects.link(curve_obj)\r\n \r\n return curve_obj\r\n\r\n\r\ndef create_nurbs_curve(\r\n points: list,\r\n order: int = 4,\r\n cyclic: bool = False,\r\n name: str = \"NurbsCurve\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a NURBS curve from points.\r\n \r\n Args:\r\n points: List of (x, y, z) tuples\r\n order: NURBS order (2-6)\r\n cyclic: Close the curve\r\n name: Object name\r\n \r\n Returns:\r\n The created curve object\r\n \"\"\"\r\n curve_data = bpy.data.curves.new(name, type='CURVE')\r\n curve_data.dimensions = '3D'\r\n \r\n spline = curve_data.splines.new('NURBS')\r\n spline.points.add(len(points) - 1)\r\n \r\n for i, point in enumerate(points):\r\n spline.points[i].co = (*point, 1.0) # w=1\r\n \r\n spline.order_u = order\r\n spline.use_cyclic_u = cyclic\r\n spline.use_endpoint_u = True\r\n \r\n curve_obj = bpy.data.objects.new(name, curve_data)\r\n bpy.context.collection.objects.link(curve_obj)\r\n \r\n return curve_obj\r\n\r\n\r\ndef create_circle_curve(\r\n radius: float = 1.0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Circle\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a circle curve.\"\"\"\r\n bpy.ops.curve.primitive_bezier_circle_add(\r\n radius=radius,\r\n location=location\r\n )\r\n circle = bpy.context.active_object\r\n circle.name = name\r\n return circle\r\n\r\n\r\ndef create_path(\r\n length: float = 5.0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Path\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a straight path curve for animation.\"\"\"\r\n bpy.ops.curve.primitive_nurbs_path_add(location=location)\r\n path = bpy.context.active_object\r\n path.name = name\r\n path.data.path_duration = 100\r\n \r\n # Scale to length\r\n path.scale.x = length / 5\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n return path\r\n\r\n\r\ndef curve_to_mesh(curve: bpy.types.Object) -> bpy.types.Object:\r\n \"\"\"Convert curve to mesh.\"\"\"\r\n bpy.context.view_layer.objects.active = curve\r\n curve.select_set(True)\r\n bpy.ops.object.convert(target='MESH')\r\n return bpy.context.active_object\r\n\r\n\r\ndef add_bevel_to_curve(\r\n curve: bpy.types.Object,\r\n depth: float = 0.1,\r\n resolution: int = 4\r\n) -> None:\r\n \"\"\"Add bevel/thickness to curve.\"\"\"\r\n curve.data.bevel_depth = depth\r\n curve.data.bevel_resolution = resolution\r\n\r\n\r\ndef add_taper_to_curve(\r\n curve: bpy.types.Object,\r\n taper_curve: bpy.types.Object\r\n) -> None:\r\n \"\"\"Add taper object to control curve thickness.\"\"\"\r\n curve.data.taper_object = taper_curve\r\n\r\n\r\ndef extrude_curve(curve: bpy.types.Object, amount: float = 0.5) -> None:\r\n \"\"\"Extrude curve in Z direction.\"\"\"\r\n curve.data.extrude = amount\r\n\r\n\r\ndef set_curve_fill(curve: bpy.types.Object, fill_mode: str = 'FULL') -> None:\r\n \"\"\"Set curve fill mode: 'NONE', 'BACK', 'FRONT', 'FULL'.\"\"\"\r\n curve.data.fill_mode = fill_mode\r\n\r\n\r\ndef create_spiral(\r\n turns: int = 5,\r\n height: float = 2.0,\r\n start_radius: float = 1.0,\r\n end_radius: float = 0.5,\r\n points_per_turn: int = 16,\r\n name: str = \"Spiral\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a spiral curve.\r\n \r\n Args:\r\n turns: Number of complete rotations\r\n height: Total height\r\n start_radius: Starting radius\r\n end_radius: Ending radius\r\n points_per_turn: Resolution\r\n name: Object name\r\n \r\n Returns:\r\n Spiral curve object\r\n \"\"\"\r\n total_points = turns * points_per_turn\r\n points = []\r\n \r\n for i in range(total_points + 1):\r\n t = i / total_points\r\n angle = t * turns * 2 * math.pi\r\n radius = start_radius + (end_radius - start_radius) * t\r\n z = t * height\r\n \r\n x = math.cos(angle) * radius\r\n y = math.sin(angle) * radius\r\n \r\n points.append((x, y, z))\r\n \r\n return create_bezier_curve(points, name=name)\r\n\r\n\r\ndef create_helix(\r\n radius: float = 1.0,\r\n height: float = 3.0,\r\n turns: int = 5,\r\n name: str = \"Helix\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a helix (constant radius spiral).\"\"\"\r\n return create_spiral(\r\n turns=turns,\r\n height=height,\r\n start_radius=radius,\r\n end_radius=radius,\r\n name=name\r\n )","category":"utility","source_file":"curve_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Curve To Mesh. Category: utility","output":"import bpy\n\ndef curve_to_mesh(curve: bpy.types.Object) -> bpy.types.Object:\r\n \"\"\"Convert curve to mesh.\"\"\"\r\n bpy.context.view_layer.objects.active = curve\r\n curve.select_set(True)\r\n bpy.ops.object.convert(target='MESH')\r\n return bpy.context.active_object\r\n\r\n\r\ndef add_bevel_to_curve(\r\n curve: bpy.types.Object,\r\n depth: float = 0.1,\r\n resolution: int = 4\r\n) -> None:\r\n \"\"\"Add bevel/thickness to curve.\"\"\"\r\n curve.data.bevel_depth = depth\r\n curve.data.bevel_resolution = resolution\r\n\r\n\r\ndef add_taper_to_curve(\r\n curve: bpy.types.Object,\r\n taper_curve: bpy.types.Object\r\n) -> None:\r\n \"\"\"Add taper object to control curve thickness.\"\"\"\r\n curve.data.taper_object = taper_curve\r\n\r\n\r\ndef extrude_curve(curve: bpy.types.Object, amount: float = 0.5) -> None:\r\n \"\"\"Extrude curve in Z direction.\"\"\"\r\n curve.data.extrude = amount\r\n\r\n\r\ndef set_curve_fill(curve: bpy.types.Object, fill_mode: str = 'FULL') -> None:\r\n \"\"\"Set curve fill mode: 'NONE', 'BACK', 'FRONT', 'FULL'.\"\"\"\r\n curve.data.fill_mode = fill_mode\r\n\r\n\r\ndef create_spiral(\r\n turns: int = 5,\r\n height: float = 2.0,\r\n start_radius: float = 1.0,\r\n end_radius: float = 0.5,\r\n points_per_turn: int = 16,\r\n name: str = \"Spiral\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a spiral curve.\r\n \r\n Args:\r\n turns: Number of complete rotations\r\n height: Total height\r\n start_radius: Starting radius\r\n end_radius: Ending radius\r\n points_per_turn: Resolution\r\n name: Object name\r\n \r\n Returns:\r\n Spiral curve object\r\n \"\"\"\r\n total_points = turns * points_per_turn\r\n points = []\r\n \r\n for i in range(total_points + 1):\n t = i / total_points\r\n angle = t * turns * 2 * math.pi\r\n radius = start_radius + (end_radius - start_radius) * t\r\n z = t * height\r\n \r\n x = math.cos(angle) * radius\r\n y = math.sin(angle) * radius\r\n \r\n points.append((x, y, z))\r\n \r\n return create_bezier_curve(points, name=name)","category":"utility","source_file":"curve_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Data Management Utilities. Functions for managing Blender data blocks and cleanup.","output":"import bpy\r\n\r\n\r\ndef purge_orphans() -> dict:\r\n \"\"\"\r\n Remove all orphan data blocks.\r\n \r\n Returns:\r\n Dictionary with counts of removed items by type\r\n \"\"\"\r\n counts = {}\r\n \r\n # Get counts before\r\n for attr in ['meshes', 'materials', 'textures', 'images', \r\n 'curves', 'armatures', 'actions']:\r\n data = getattr(bpy.data, attr)\r\n orphans = [d for d in data if d.users == 0]\r\n if orphans:\r\n counts[attr] = len(orphans)\r\n \r\n # Purge\r\n bpy.ops.outliner.orphans_purge(do_recursive=True)\r\n \r\n return counts\r\n\r\n\r\ndef list_unused_materials() -> list:\r\n \"\"\"List materials with no users.\"\"\"\r\n return [mat.name for mat in bpy.data.materials if mat.users == 0]\r\n\r\n\r\ndef list_unused_images() -> list:\r\n \"\"\"List images with no users.\"\"\"\r\n return [img.name for img in bpy.data.images if img.users == 0]\r\n\r\n\r\ndef remove_material(name: str) -> bool:\r\n \"\"\"Remove material by name.\"\"\"\r\n mat = bpy.data.materials.get(name)\r\n if mat:\r\n bpy.data.materials.remove(mat)\r\n return True\r\n return False\r\n\r\n\r\ndef remove_image(name: str) -> bool:\r\n \"\"\"Remove image by name.\"\"\"\r\n img = bpy.data.images.get(name)\r\n if img:\r\n bpy.data.images.remove(img)\r\n return True\r\n return False\r\n\r\n\r\ndef remove_mesh(name: str) -> bool:\r\n \"\"\"Remove mesh data by name.\"\"\"\r\n mesh = bpy.data.meshes.get(name)\r\n if mesh:\r\n bpy.data.meshes.remove(mesh)\r\n return True\r\n return False\r\n\r\n\r\ndef duplicate_data_block(\r\n data_block: bpy.types.ID,\r\n new_name: str = None\r\n) -> bpy.types.ID:\r\n \"\"\"\r\n Duplicate any data block.\r\n \r\n Args:\r\n data_block: Any Blender data block\r\n new_name: Optional new name\r\n \r\n Returns:\r\n Duplicated data block\r\n \"\"\"\r\n copy = data_block.copy()\r\n if new_name:\r\n copy.name = new_name\r\n return copy\r\n\r\n\r\ndef rename_data_block(\r\n data_type: str,\r\n old_name: str,\r\n new_name: str\r\n) -> bool:\r\n \"\"\"\r\n Rename a data block.\r\n \r\n Args:\r\n data_type: 'objects', 'materials', 'meshes', 'images', etc.\r\n old_name: Current name\r\n new_name: New name\r\n \r\n Returns:\r\n Success status\r\n \"\"\"\r\n data = getattr(bpy.data, data_type, None)\r\n if data:\r\n item = data.get(old_name)\r\n if item:\r\n item.name = new_name\r\n return True\r\n return False\r\n\r\n\r\ndef make_local(obj: bpy.types.Object, data: bool = True) -> None:\r\n \"\"\"Make linked object local.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.make_local(type='ALL' if data else 'SELECT_OBJECT')\r\n\r\n\r\ndef make_single_user(\r\n obj: bpy.types.Object,\r\n object_data: bool = True,\r\n material: bool = True,\r\n animation: bool = True\r\n) -> None:\r\n \"\"\"Make object data single-user (unlink from other objects).\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.make_single_user(\r\n type='SELECTED_OBJECTS',\r\n object=True,\r\n obdata=object_data,\r\n material=material,\r\n animation=animation\r\n )\r\n\r\n\r\ndef pack_all_external() -> None:\r\n \"\"\"Pack all external files into blend file.\"\"\"\r\n bpy.ops.file.pack_all()\r\n\r\n\r\ndef unpack_all(method: str = 'USE_ORIGINAL') -> None:\r\n \"\"\"\r\n Unpack all external files.\r\n \r\n Args:\r\n method: 'USE_ORIGINAL', 'WRITE_ORIGINAL', 'USE_LOCAL', 'WRITE_LOCAL'\r\n \"\"\"\r\n bpy.ops.file.unpack_all(method=method)\r\n\r\n\r\ndef get_data_stats() -> dict:\r\n \"\"\"Get statistics about scene data.\"\"\"\r\n return {\r\n 'objects': len(bpy.data.objects),\r\n 'meshes': len(bpy.data.meshes),\r\n 'materials': len(bpy.data.materials),\r\n 'textures': len(bpy.data.textures),\r\n 'images': len(bpy.data.images),\r\n 'armatures': len(bpy.data.armatures),\r\n 'actions': len(bpy.data.actions),\r\n 'curves': len(bpy.data.curves),\r\n 'cameras': len(bpy.data.cameras),\r\n 'lights': len(bpy.data.lights),\r\n 'worlds': len(bpy.data.worlds),\r\n 'node_groups': len(bpy.data.node_groups)\r\n }\r\n\r\n\r\ndef list_linked_libraries() -> list:\r\n \"\"\"List all linked library paths.\"\"\"\r\n return [lib.filepath for lib in bpy.data.libraries]\r\n\r\n\r\ndef reload_linked_library(filepath: str) -> bool:\r\n \"\"\"Reload a linked library.\"\"\"\r\n lib = [l for l in bpy.data.libraries if l.filepath == filepath]\r\n if lib:\r\n lib[0].reload()\r\n return True\r\n return False\r\n\r\n\r\ndef merge_duplicate_materials(threshold: float = 0.01) -> int:\r\n \"\"\"\r\n Merge materials with identical settings.\r\n \r\n Returns:\r\n Number of materials merged\r\n \"\"\"\r\n merged = 0\r\n # This is a simplified approach - full implementation would\r\n # compare actual node setups\r\n seen = {}\r\n \r\n for obj in bpy.data.objects:\r\n if obj.type != 'MESH':\r\n continue\r\n \r\n for slot in obj.material_slots:\r\n if not slot.material:\r\n continue\r\n \r\n mat = slot.material\r\n base_name = mat.name.rsplit('.', 1)[0]\r\n \r\n if base_name in seen:\r\n slot.material = seen[base_name]\r\n merged += 1\r\n else:\r\n seen[base_name] = mat\r\n \r\n return merged","category":"utility","source_file":"data_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Purge Orphans. Category: utility","output":"import bpy\n\ndef purge_orphans() -> dict:\r\n \"\"\"\r\n Remove all orphan data blocks.\r\n \r\n Returns:\r\n Dictionary with counts of removed items by type\r\n \"\"\"\r\n counts = {}\r\n \r\n # Get counts before\r\n for attr in ['meshes', 'materials', 'textures', 'images', \r\n 'curves', 'armatures', 'actions']:\r\n data = getattr(bpy.data, attr)\r\n orphans = [d for d in data if d.users == 0]\r\n if orphans:\r\n counts[attr] = len(orphans)\r\n \r\n # Purge\r\n bpy.ops.outliner.orphans_purge(do_recursive=True)\r\n \r\n return counts\r\n\r\n\r\ndef list_unused_materials() -> list:\r\n \"\"\"List materials with no users.\"\"\"\r\n return [mat.name for mat in bpy.data.materials if mat.users == 0]\r\n\r\n\r\ndef list_unused_images() -> list:\r\n \"\"\"List images with no users.\"\"\"\r\n return [img.name for img in bpy.data.images if img.users == 0]\r\n\r\n\r\ndef remove_material(name: str) -> bool:\r\n \"\"\"Remove material by name.\"\"\"\r\n mat = bpy.data.materials.get(name)\r\n if mat:\r\n bpy.data.materials.remove(mat)\r\n return True\r\n return False\r\n\r\n\r\ndef remove_image(name: str) -> bool:\r\n \"\"\"Remove image by name.\"\"\"\r\n img = bpy.data.images.get(name)\r\n if img:\r\n bpy.data.images.remove(img)\r\n return True\r\n return False\r\n\r\n\r\ndef remove_mesh(name: str) -> bool:\r\n \"\"\"Remove mesh data by name.\"\"\"\r\n mesh = bpy.data.meshes.get(name)\r\n if mesh:\r\n bpy.data.meshes.remove(mesh)\r\n return True\r\n return False\r\n\r\n\r\ndef duplicate_data_block(\r\n data_block: bpy.types.ID,\r\n new_name: str = None\r\n) -> bpy.types.ID:\r\n \"\"\"\r\n Duplicate any data block.\r\n \r\n Args:\r\n data_block: Any Blender data block\r\n new_name: Optional new name\r\n \r\n Returns:\r\n Duplicated data block\r\n \"\"\"\r\n copy = data_block.copy()\r\n if new_name:\r\n copy.name = new_name\r\n return copy\r\n\r\n\r\ndef rename_data_block(\r\n data_type: str,\r\n old_name: str,\r\n new_name: str\r\n) -> bool:\r\n \"\"\"\r\n Rename a data block.\r\n \r\n Args:\r\n data_type: 'objects', 'materials', 'meshes', 'images', etc.\r\n old_name: Current name\r\n new_name: New name\r\n \r\n Returns:\r\n Success status\r\n \"\"\"\r\n data = getattr(bpy.data, data_type, None)\r\n if data:\r\n item = data.get(old_name)\r\n if item:\r\n item.name = new_name\r\n return True\r\n return False\r\n\r\n\r\ndef make_local(obj: bpy.types.Object, data: bool = True) -> None:\r\n \"\"\"Make linked object local.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.make_local(type='ALL' if data else 'SELECT_OBJECT')\r\n\r\n\r\ndef make_single_user(\r\n obj: bpy.types.Object,\r\n object_data: bool = True,\r\n material: bool = True,\r\n animation: bool = True\r\n) -> None:\r\n \"\"\"Make object data single-user (unlink from other objects).\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.make_single_user(\r\n type='SELECTED_OBJECTS',\r\n object=True,\r\n obdata=object_data,\r\n material=material,\r\n animation=animation\r\n )\r\n\r\n\r\ndef pack_all_external() -> None:\r\n \"\"\"Pack all external files into blend file.\"\"\"\r\n bpy.ops.file.pack_all()\r\n\r\n\r\ndef unpack_all(method: str = 'USE_ORIGINAL') -> None:\r\n \"\"\"\r\n Unpack all external files.\r\n \r\n Args:\r\n method: 'USE_ORIGINAL', 'WRITE_ORIGINAL', 'USE_LOCAL', 'WRITE_LOCAL'\r\n \"\"\"\r\n bpy.ops.file.unpack_all(method=method)\r\n\r\n\r\ndef get_data_stats() -> dict:\r\n \"\"\"Get statistics about scene data.\"\"\"\r\n return {\r\n 'objects': len(bpy.data.objects),\r\n 'meshes': len(bpy.data.meshes),\r\n 'materials': len(bpy.data.materials),\r\n 'textures': len(bpy.data.textures),\r\n 'images': len(bpy.data.images),\r\n 'armatures': len(bpy.data.armatures),\r\n 'actions': len(bpy.data.actions),\r\n 'curves': len(bpy.data.curves),\r\n 'cameras': len(bpy.data.cameras),\r\n 'lights': len(bpy.data.lights),\r\n 'worlds': len(bpy.data.worlds),\r\n 'node_groups': len(bpy.data.node_groups)\r\n }\r\n\r\n\r\ndef list_linked_libraries() -> list:\r\n \"\"\"List all linked library paths.\"\"\"\r\n return [lib.filepath for lib in bpy.data.libraries]\r\n\r\n\r\ndef reload_linked_library(filepath: str) -> bool:\r\n \"\"\"Reload a linked library.\"\"\"\r\n lib = [l for l in bpy.data.libraries if l.filepath == filepath]\r\n if lib:\r\n lib[0].reload()\r\n return True\r\n return False\r\n\r\n\r\ndef merge_duplicate_materials(threshold: float = 0.01) -> int:\r\n \"\"\"\r\n Merge materials with identical settings.\r\n \r\n Returns:\r\n Number of materials merged\r\n \"\"\"\r\n merged = 0\r\n # This is a simplified approach - full implementation would\r\n # compare actual node setups\r\n seen = {}\r\n \r\n for obj in bpy.data.objects:\r\n if obj.type != 'MESH':\r\n continue\r\n \r\n for slot in obj.material_slots:\r\n if not slot.material:\r\n continue\r\n \r\n mat = slot.material\r\n base_name = mat.name.rsplit('.', 1)[0]\r\n \r\n if base_name in seen:\r\n slot.material = seen[base_name]\r\n merged += 1\r\n else:\r\n seen[base_name] = mat\r\n \r\n return merged","category":"utility","source_file":"data_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Displacement & Surface Texture Recipes β€” Blender 4.0/5.0. Procedural displacement patterns for terrain, raked sand, water ripples, rocky surfaces, and other surface deformations using shader nodes. TWO METHODS FOR SURFACE DEFORMATION:","output":"import bpy\r\n\r\n\r\n# =============================================================================\r\n# SHADER-BASED DISPLACEMENT (Cycles)\r\n# =============================================================================\r\n\r\ndef create_raked_sand_material(name=\"RakedSand\", scale=8.0, distortion=2.0):\r\n \"\"\"Create raked sand/zen garden material with wave displacement.\r\n\r\n Uses ShaderNodeTexWave (BANDS, SIN profile) for parallel rake lines,\r\n mixed with subtle noise for natural variation.\r\n Connects to Material Output Displacement socket for true displacement.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.displacement_method = 'BOTH' # Bump + true displacement\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Output + Principled\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (200, 0)\r\n bsdf.inputs['Base Color'].default_value = (0.76, 0.70, 0.58, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.85\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n # Texture Coordinates\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n\r\n # Wave Texture β€” parallel rake lines\r\n wave = nodes.new('ShaderNodeTexWave')\r\n wave.location = (-400, 100)\r\n wave.wave_type = 'BANDS'\r\n wave.wave_profile = 'SIN'\r\n wave.bands_direction = 'X'\r\n wave.inputs['Scale'].default_value = scale\r\n wave.inputs['Distortion'].default_value = distortion\r\n wave.inputs['Detail'].default_value = 3.0\r\n wave.inputs['Detail Scale'].default_value = 1.5\r\n\r\n # Noise for natural variation\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, -100)\r\n noise.inputs['Scale'].default_value = 25.0\r\n noise.inputs['Detail'].default_value = 4.0\r\n noise.inputs['Roughness'].default_value = 0.6\r\n\r\n # Mix wave + noise\r\n mix = nodes.new('ShaderNodeMix')\r\n mix.location = (-200, 0)\r\n mix.data_type = 'FLOAT'\r\n mix.inputs[0].default_value = 0.7 # Factor: mostly wave\r\n links.new(wave.outputs['Fac'], mix.inputs[2]) # A\r\n links.new(noise.outputs['Fac'], mix.inputs[3]) # B\r\n\r\n # Displacement node\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (200, -200)\r\n disp.inputs['Scale'].default_value = 0.05\r\n disp.inputs['Midlevel'].default_value = 0.5\r\n\r\n links.new(tex_coord.outputs['Object'], wave.inputs['Vector'])\r\n links.new(tex_coord.outputs['Object'], noise.inputs['Vector'])\r\n links.new(mix.outputs[0], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat\r\n\r\n\r\ndef create_water_ripple_material(name=\"WaterRipple\"):\r\n \"\"\"Create concentric water ripple displacement using RINGS wave type.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.displacement_method = 'BOTH'\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (200, 0)\r\n bsdf.inputs['Base Color'].default_value = (0.1, 0.3, 0.5, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.05\r\n bsdf.inputs['Transmission Weight'].default_value = 0.6\r\n bsdf.inputs['IOR'].default_value = 1.33\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n wave = nodes.new('ShaderNodeTexWave')\r\n wave.location = (-400, 0)\r\n wave.wave_type = 'RINGS'\r\n wave.wave_profile = 'SIN'\r\n wave.inputs['Scale'].default_value = 12.0\r\n wave.inputs['Distortion'].default_value = 1.0\r\n wave.inputs['Detail'].default_value = 2.0\r\n\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (200, -200)\r\n disp.inputs['Scale'].default_value = 0.03\r\n\r\n links.new(wave.outputs['Fac'], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat\r\n\r\n\r\ndef create_rocky_surface_material(name=\"RockySurface\"):\r\n \"\"\"Create rough rocky terrain using noise displacement + bump.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.displacement_method = 'BOTH'\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (200, 0)\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.30, 0.25, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n\r\n # Large-scale noise for terrain shape\r\n noise_large = nodes.new('ShaderNodeTexNoise')\r\n noise_large.location = (-400, 100)\r\n noise_large.inputs['Scale'].default_value = 3.0\r\n noise_large.inputs['Detail'].default_value = 8.0\r\n noise_large.inputs['Roughness'].default_value = 0.7\r\n\r\n # Small-scale noise for surface detail\r\n noise_small = nodes.new('ShaderNodeTexNoise')\r\n noise_small.location = (-400, -100)\r\n noise_small.inputs['Scale'].default_value = 20.0\r\n noise_small.inputs['Detail'].default_value = 12.0\r\n noise_small.inputs['Roughness'].default_value = 0.8\r\n\r\n # Mix large + small\r\n mix = nodes.new('ShaderNodeMix')\r\n mix.location = (-200, 0)\r\n mix.data_type = 'FLOAT'\r\n mix.inputs[0].default_value = 0.3\r\n links.new(noise_large.outputs['Fac'], mix.inputs[2])\r\n links.new(noise_small.outputs['Fac'], mix.inputs[3])\r\n\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (200, -200)\r\n disp.inputs['Scale'].default_value = 0.15\r\n\r\n links.new(tex_coord.outputs['Object'], noise_large.inputs['Vector'])\r\n links.new(tex_coord.outputs['Object'], noise_small.inputs['Vector'])\r\n links.new(mix.outputs[0], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat\r\n\r\n\r\n# =============================================================================\r\n# MODIFIER-BASED DISPLACEMENT (works in EEVEE + viewport)\r\n# =============================================================================\r\n\r\ndef add_wave_displacement_modifier(obj, texture_type='WAVES',\r\n scale=0.1, subdivisions=4):\r\n \"\"\"Add modifier-based displacement using Subdivision + Displace.\r\n\r\n This method works in EEVEE and is visible in the viewport.\r\n Good for: terrain, raked sand, rippled surfaces.\r\n\r\n Args:\r\n obj: Target mesh object\r\n texture_type: 'WAVES', 'NOISE', 'VORONOI', 'CLOUDS'\r\n scale: Displacement strength\r\n subdivisions: Subdivision level (higher = smoother)\r\n \"\"\"\r\n # Add subdivision for geometry detail\r\n subsurf = obj.modifiers.new(name=\"Subdiv\", type='SUBSURF')\r\n subsurf.levels = subdivisions\r\n subsurf.render_levels = subdivisions + 1\r\n\r\n # Create displacement texture\r\n tex = bpy.data.textures.new(name=f\"{obj.name}_DispTex\", type=texture_type)\r\n\r\n if texture_type == 'WAVES':\r\n # Blender internal texture for modifier displacement\r\n # Note: This uses bpy.data.textures, NOT shader nodes\r\n pass # WAVES type has built-in wave pattern\r\n elif texture_type == 'NOISE':\r\n tex.noise_scale = 0.5\r\n elif texture_type == 'VORONOI':\r\n tex.noise_intensity = 1.0\r\n elif texture_type == 'CLOUDS':\r\n tex.noise_scale = 0.5\r\n tex.noise_depth = 3\r\n\r\n # Add displace modifier\r\n displace = obj.modifiers.new(name=\"Displace\", type='DISPLACE')\r\n displace.texture = tex\r\n displace.strength = scale\r\n displace.mid_level = 0.5\r\n\r\n return subsurf, displace\r\n\r\n\r\ndef create_terrain_plane(name=\"Terrain\", size=10, subdivisions=5,\r\n noise_scale=0.3):\r\n \"\"\"Create a subdivided plane with noise displacement for terrain.\r\n\r\n Creates a ready-to-use terrain mesh with modifier-based displacement.\r\n Works in both EEVEE and Cycles, visible in viewport.\r\n \"\"\"\r\n # Create subdivided plane\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=(0, 0, 0))\r\n terrain = bpy.context.active_object\r\n terrain.name = name\r\n\r\n # Add subdivision + displacement\r\n add_wave_displacement_modifier(\r\n terrain,\r\n texture_type='NOISE',\r\n scale=noise_scale,\r\n subdivisions=subdivisions\r\n )\r\n\r\n # Add earth-tone material\r\n mat = bpy.data.materials.new(name=f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.32, 0.2, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n terrain.data.materials.append(mat)\r\n\r\n return terrain\r\n\r\n\r\ndef create_raked_sand_plane(name=\"RakedSandFloor\", size=10):\r\n \"\"\"Create a zen garden raked sand floor plane.\r\n\r\n Uses modifier-based displacement visible in EEVEE and viewport.\r\n Combines wave pattern (rake lines) with subtle noise (natural grain).\r\n \"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=(0, 0, 0))\r\n sand = bpy.context.active_object\r\n sand.name = name\r\n\r\n # Subdivision for detail\r\n subsurf = sand.modifiers.new(name=\"Subdiv\", type='SUBSURF')\r\n subsurf.levels = 5\r\n subsurf.render_levels = 6\r\n\r\n # Wave displacement for rake lines\r\n wave_tex = bpy.data.textures.new(name=\"RakeWaves\", type='WAVES')\r\n wave_disp = sand.modifiers.new(name=\"RakeLines\", type='DISPLACE')\r\n wave_disp.texture = wave_tex\r\n wave_disp.strength = 0.04\r\n wave_disp.mid_level = 0.5\r\n\r\n # Subtle noise for natural grain\r\n noise_tex = bpy.data.textures.new(name=\"SandGrain\", type='NOISE')\r\n noise_disp = sand.modifiers.new(name=\"SandGrain\", type='DISPLACE')\r\n noise_disp.texture = noise_tex\r\n noise_disp.strength = 0.01\r\n noise_disp.mid_level = 0.5\r\n\r\n # Sand material\r\n mat = bpy.data.materials.new(name=\"SandMaterial\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.76, 0.70, 0.58, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.85\r\n sand.data.materials.append(mat)\r\n\r\n return sand","category":"utility","source_file":"displacement_textures.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Create raked sand/zen garden material with wave displacement.\r\n\r\n Uses ShaderNodeTexWave (BANDS, SIN profile) for parallel rake lines,\r\n mixed with subtle noise for natural variation.\r\n Connects to Material Output Displacement socket for true displacement.","output":"import bpy\n\ndef create_raked_sand_material(name=\"RakedSand\", scale=8.0, distortion=2.0):\n \"\"\"Create raked sand/zen garden material with wave displacement.\r\n\r\n Uses ShaderNodeTexWave (BANDS, SIN profile) for parallel rake lines,\r\n mixed with subtle noise for natural variation.\r\n Connects to Material Output Displacement socket for true displacement.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.displacement_method = 'BOTH' # Bump + true displacement\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Output + Principled\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (200, 0)\r\n bsdf.inputs['Base Color'].default_value = (0.76, 0.70, 0.58, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.85\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n # Texture Coordinates\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n\r\n # Wave Texture β€” parallel rake lines\r\n wave = nodes.new('ShaderNodeTexWave')\r\n wave.location = (-400, 100)\r\n wave.wave_type = 'BANDS'\r\n wave.wave_profile = 'SIN'\r\n wave.bands_direction = 'X'\r\n wave.inputs['Scale'].default_value = scale\r\n wave.inputs['Distortion'].default_value = distortion\r\n wave.inputs['Detail'].default_value = 3.0\r\n wave.inputs['Detail Scale'].default_value = 1.5\r\n\r\n # Noise for natural variation\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, -100)\r\n noise.inputs['Scale'].default_value = 25.0\r\n noise.inputs['Detail'].default_value = 4.0\r\n noise.inputs['Roughness'].default_value = 0.6\r\n\r\n # Mix wave + noise\r\n mix = nodes.new('ShaderNodeMix')\r\n mix.location = (-200, 0)\r\n mix.data_type = 'FLOAT'\r\n mix.inputs[0].default_value = 0.7 # Factor: mostly wave\r\n links.new(wave.outputs['Fac'], mix.inputs[2]) # A\r\n links.new(noise.outputs['Fac'], mix.inputs[3]) # B\r\n\r\n # Displacement node\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (200, -200)\r\n disp.inputs['Scale'].default_value = 0.05\r\n disp.inputs['Midlevel'].default_value = 0.5\r\n\r\n links.new(tex_coord.outputs['Object'], wave.inputs['Vector'])\r\n links.new(tex_coord.outputs['Object'], noise.inputs['Vector'])\r\n links.new(mix.outputs[0], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat","category":"utility","source_file":"displacement_textures.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create concentric water ripple displacement using RINGS wave type.","output":"import bpy\n\ndef create_water_ripple_material(name=\"WaterRipple\"):\n \"\"\"Create concentric water ripple displacement using RINGS wave type.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.displacement_method = 'BOTH'\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (200, 0)\r\n bsdf.inputs['Base Color'].default_value = (0.1, 0.3, 0.5, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.05\r\n bsdf.inputs['Transmission Weight'].default_value = 0.6\r\n bsdf.inputs['IOR'].default_value = 1.33\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n wave = nodes.new('ShaderNodeTexWave')\r\n wave.location = (-400, 0)\r\n wave.wave_type = 'RINGS'\r\n wave.wave_profile = 'SIN'\r\n wave.inputs['Scale'].default_value = 12.0\r\n wave.inputs['Distortion'].default_value = 1.0\r\n wave.inputs['Detail'].default_value = 2.0\r\n\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (200, -200)\r\n disp.inputs['Scale'].default_value = 0.03\r\n\r\n links.new(wave.outputs['Fac'], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat","category":"utility","source_file":"displacement_textures.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create rough rocky terrain using noise displacement + bump.","output":"import bpy\n\ndef create_rocky_surface_material(name=\"RockySurface\"):\n \"\"\"Create rough rocky terrain using noise displacement + bump.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.displacement_method = 'BOTH'\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (200, 0)\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.30, 0.25, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n\r\n # Large-scale noise for terrain shape\r\n noise_large = nodes.new('ShaderNodeTexNoise')\r\n noise_large.location = (-400, 100)\r\n noise_large.inputs['Scale'].default_value = 3.0\r\n noise_large.inputs['Detail'].default_value = 8.0\r\n noise_large.inputs['Roughness'].default_value = 0.7\r\n\r\n # Small-scale noise for surface detail\r\n noise_small = nodes.new('ShaderNodeTexNoise')\r\n noise_small.location = (-400, -100)\r\n noise_small.inputs['Scale'].default_value = 20.0\r\n noise_small.inputs['Detail'].default_value = 12.0\r\n noise_small.inputs['Roughness'].default_value = 0.8\r\n\r\n # Mix large + small\r\n mix = nodes.new('ShaderNodeMix')\r\n mix.location = (-200, 0)\r\n mix.data_type = 'FLOAT'\r\n mix.inputs[0].default_value = 0.3\r\n links.new(noise_large.outputs['Fac'], mix.inputs[2])\r\n links.new(noise_small.outputs['Fac'], mix.inputs[3])\r\n\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (200, -200)\r\n disp.inputs['Scale'].default_value = 0.15\r\n\r\n links.new(tex_coord.outputs['Object'], noise_large.inputs['Vector'])\r\n links.new(tex_coord.outputs['Object'], noise_small.inputs['Vector'])\r\n links.new(mix.outputs[0], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat","category":"utility","source_file":"displacement_textures.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create a zen garden raked sand floor plane.\r\n\r\n Uses modifier-based displacement visible in EEVEE and viewport.\r\n Combines wave pattern (rake lines) with subtle noise (natural grain).","output":"import bpy\n\ndef create_raked_sand_plane(name=\"RakedSandFloor\", size=10):\n \"\"\"Create a zen garden raked sand floor plane.\r\n\r\n Uses modifier-based displacement visible in EEVEE and viewport.\r\n Combines wave pattern (rake lines) with subtle noise (natural grain).\r\n \"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=(0, 0, 0))\r\n sand = bpy.context.active_object\r\n sand.name = name\r\n\r\n # Subdivision for detail\r\n subsurf = sand.modifiers.new(name=\"Subdiv\", type='SUBSURF')\r\n subsurf.levels = 5\r\n subsurf.render_levels = 6\r\n\r\n # Wave displacement for rake lines\r\n wave_tex = bpy.data.textures.new(name=\"RakeWaves\", type='WAVES')\r\n wave_disp = sand.modifiers.new(name=\"RakeLines\", type='DISPLACE')\r\n wave_disp.texture = wave_tex\r\n wave_disp.strength = 0.04\r\n wave_disp.mid_level = 0.5\r\n\r\n # Subtle noise for natural grain\r\n noise_tex = bpy.data.textures.new(name=\"SandGrain\", type='NOISE')\r\n noise_disp = sand.modifiers.new(name=\"SandGrain\", type='DISPLACE')\r\n noise_disp.texture = noise_tex\r\n noise_disp.strength = 0.01\r\n noise_disp.mid_level = 0.5\r\n\r\n # Sand material\r\n mat = bpy.data.materials.new(name=\"SandMaterial\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.76, 0.70, 0.58, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.85\r\n sand.data.materials.append(mat)\r\n\r\n return sand","category":"utility","source_file":"displacement_textures.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Driver Utilities. Functions for creating and managing property drivers.","output":"import bpy\r\n\r\n\r\ndef add_driver(\r\n obj: bpy.types.Object,\r\n data_path: str,\r\n expression: str,\r\n index: int = -1\r\n) -> bpy.types.Driver:\r\n \"\"\"\r\n Add a driver with expression.\r\n \r\n Args:\r\n obj: Object to add driver to\r\n data_path: Property path (e.g., 'location', 'rotation_euler')\r\n expression: Python expression\r\n index: Array index (-1 for non-array)\r\n \r\n Returns:\r\n The created driver\r\n \r\n Example:\r\n >>> add_driver(cube, 'location', 'sin(frame/10)', index=2)\r\n \"\"\"\r\n if index >= 0:\r\n fcurve = obj.driver_add(data_path, index)\r\n else:\r\n fcurve = obj.driver_add(data_path)\r\n \r\n driver = fcurve.driver\r\n driver.type = 'SCRIPTED'\r\n driver.expression = expression\r\n \r\n return driver\r\n\r\n\r\ndef add_driver_variable(\r\n driver: bpy.types.Driver,\r\n name: str,\r\n target: bpy.types.Object,\r\n data_path: str,\r\n var_type: str = 'SINGLE_PROP'\r\n) -> bpy.types.DriverVariable:\r\n \"\"\"\r\n Add a variable to a driver.\r\n \r\n Args:\r\n driver: Target driver\r\n name: Variable name\r\n target: Source object\r\n data_path: Property path on target\r\n var_type: 'SINGLE_PROP', 'TRANSFORMS', 'ROTATION_DIFF', 'LOC_DIFF'\r\n \r\n Returns:\r\n The created variable\r\n \"\"\"\r\n var = driver.variables.new()\r\n var.name = name\r\n var.type = var_type\r\n \r\n if var_type == 'SINGLE_PROP':\r\n var.targets[0].id = target\r\n var.targets[0].data_path = data_path\r\n elif var_type == 'TRANSFORMS':\r\n var.targets[0].id = target\r\n var.targets[0].transform_type = 'LOC_X'\r\n var.targets[0].transform_space = 'WORLD_SPACE'\r\n \r\n return var\r\n\r\n\r\ndef add_frame_driver(\r\n obj: bpy.types.Object,\r\n data_path: str,\r\n expression: str,\r\n index: int = -1\r\n) -> bpy.types.Driver:\r\n \"\"\"\r\n Add driver using frame number.\r\n \r\n Args:\r\n obj: Target object\r\n data_path: Property path\r\n expression: Expression using 'frame' variable\r\n index: Array index\r\n \r\n Example:\r\n >>> add_frame_driver(cube, 'rotation_euler', 'frame * 0.05', index=2)\r\n \"\"\"\r\n driver = add_driver(obj, data_path, expression, index)\r\n return driver\r\n\r\n\r\ndef add_oscillation_driver(\r\n obj: bpy.types.Object,\r\n data_path: str,\r\n amplitude: float = 1.0,\r\n frequency: float = 1.0,\r\n offset: float = 0.0,\r\n index: int = -1\r\n) -> bpy.types.Driver:\r\n \"\"\"\r\n Add sine wave oscillation driver.\r\n \r\n Args:\r\n obj: Target object\r\n data_path: Property path\r\n amplitude: Wave amplitude\r\n frequency: Wave frequency\r\n offset: Base value offset\r\n index: Array index\r\n \"\"\"\r\n expression = f\"sin(frame * {frequency} * 0.1) * {amplitude} + {offset}\"\r\n return add_driver(obj, data_path, expression, index)\r\n\r\n\r\ndef add_noise_driver(\r\n obj: bpy.types.Object,\r\n data_path: str,\r\n scale: float = 1.0,\r\n strength: float = 1.0,\r\n index: int = -1\r\n) -> bpy.types.Driver:\r\n \"\"\"\r\n Add noise-based driver.\r\n \r\n Args:\r\n obj: Target object\r\n data_path: Property path\r\n scale: Noise scale\r\n strength: Effect strength\r\n index: Array index\r\n \"\"\"\r\n expression = f\"noise.random() * {strength}\"\r\n return add_driver(obj, data_path, expression, index)\r\n\r\n\r\ndef copy_property_driver(\r\n source_obj: bpy.types.Object,\r\n target_obj: bpy.types.Object,\r\n source_path: str,\r\n target_path: str,\r\n multiplier: float = 1.0,\r\n offset: float = 0.0,\r\n index: int = -1\r\n) -> bpy.types.Driver:\r\n \"\"\"\r\n Create driver that copies property from another object.\r\n \r\n Args:\r\n source_obj: Object to read from\r\n target_obj: Object to drive\r\n source_path: Property to read\r\n target_path: Property to drive\r\n multiplier: Value multiplier\r\n offset: Value offset\r\n index: Array index\r\n \"\"\"\r\n driver = add_driver(target_obj, target_path, f\"var * {multiplier} + {offset}\", index)\r\n add_driver_variable(driver, 'var', source_obj, source_path)\r\n return driver\r\n\r\n\r\ndef remove_driver(\r\n obj: bpy.types.Object,\r\n data_path: str,\r\n index: int = -1\r\n) -> None:\r\n \"\"\"Remove driver from property.\"\"\"\r\n if index >= 0:\r\n obj.driver_remove(data_path, index)\r\n else:\r\n obj.driver_remove(data_path)\r\n\r\n\r\ndef remove_all_drivers(obj: bpy.types.Object) -> None:\r\n \"\"\"Remove all drivers from object.\"\"\"\r\n if obj.animation_data:\r\n for driver in obj.animation_data.drivers[:]:\r\n obj.driver_remove(driver.data_path, driver.array_index)","category":"utility","source_file":"driver_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Remove All Drivers. Category: utility","output":"import bpy\n\ndef remove_all_drivers(obj: bpy.types.Object) -> None:\r\n \"\"\"Remove all drivers from object.\"\"\"\r\n if obj.animation_data:\r\n for driver in obj.animation_data.drivers[:]:\r\n obj.driver_remove(driver.data_path, driver.array_index)","category":"utility","source_file":"driver_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Emission Material Best Practices. Best practices for creating emissive materials in Blender that retain their color in Material Preview mode. Covers suns, neon lights, fire, lava, glowing screens, and more.","output":"import bpy\r\nimport math\r\n\r\n\r\n# =============================================================================\r\n# EMISSION MATERIAL BEST PRACTICES\r\n# =============================================================================\r\n#\r\n# PROBLEM: When Emission Strength is too high (>10), the material appears\r\n# white in Material Preview mode because the emission overwhelms the color.\r\n# This is especially common with suns, stars, and glowing objects.\r\n#\r\n# SOLUTION: Always use these rules:\r\n# 1. Set BOTH \"Base Color\" AND \"Emission Color\" to the SAME saturated color\r\n# 2. Keep Emission Strength between 3–8 for Material Preview\r\n# 3. For stronger glow, add a Point Light INSIDE or near the object instead\r\n# 4. Never rely on emission alone β€” always set a vibrant Base Color too\r\n#\r\n# RECOMMENDED EMISSION STRENGTHS:\r\n# - Subtle glow (screens, indicators): 1–3\r\n# - Medium glow (neon signs, lamps): 3–6\r\n# - Strong glow (fire, lava, stars): 5–8\r\n# - Point light supplement: Add a Point Light with energy 500–2000\r\n#\r\n# BAD: emission_strength=15, base_color=(1,1,1) β†’ appears pure white\r\n# GOOD: emission_strength=5, base_color=(1,0.85,0.2) + emission_color=(1,0.85,0.2) β†’ visible yellow\r\n# =============================================================================\r\n\r\n\r\ndef create_glow_material(\r\n name: str,\r\n color: tuple,\r\n emission_strength: float = 5.0,\r\n roughness: float = 0.5\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Create an emissive material that retains its color in Material Preview.\r\n \r\n Sets both Base Color and Emission Color to the same saturated value,\r\n preventing the white-wash effect at moderate emission strengths.\r\n \r\n Args:\r\n name: Material name\r\n color: RGB or RGBA tuple β€” use SATURATED colors, not white\r\n emission_strength: 3–8 recommended. Above 10 washes out in viewport.\r\n roughness: Surface roughness (lower = shinier glow surface)\r\n \r\n Returns:\r\n The created material\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n rgba = color if len(color) == 4 else (*color, 1.0)\r\n \r\n if bsdf:\r\n # Set base color to retain color identity even without emission\r\n bsdf.inputs['Base Color'].default_value = rgba\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n \r\n # Set emission color to SAME saturated color\r\n bsdf.inputs['Emission Color'].default_value = rgba\r\n bsdf.inputs['Emission Strength'].default_value = emission_strength\r\n \r\n return mat\r\n\r\n\r\ndef create_sun_material(\r\n name: str = \"Sun_Material\",\r\n color: tuple = (1.0, 0.85, 0.2),\r\n strength: float = 5.0\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Create a sun/star material that glows yellow (not white).\r\n \r\n Uses moderate emission strength (5) and supplements with a Point Light\r\n inside the sphere for scene illumination.\r\n \r\n Pattern:\r\n sun_mat = create_sun_material(\"Sun\", (1.0, 0.85, 0.2), strength=5)\r\n sun_obj.data.materials.append(sun_mat)\r\n \r\n # Add a point light inside for actual illumination\r\n bpy.ops.object.light_add(type='POINT', location=sun_obj.location)\r\n light = bpy.context.active_object\r\n light.data.energy = 1000\r\n light.data.color = (1.0, 0.9, 0.6)\r\n \"\"\"\r\n return create_glow_material(name, color, emission_strength=strength, roughness=0.0)\r\n\r\n\r\ndef create_neon_material(\r\n name: str = \"Neon_Material\",\r\n color: tuple = (0.1, 0.3, 1.0),\r\n strength: float = 5.0\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Create a neon sign / tube light material.\r\n \r\n Neon colors should be highly saturated:\r\n Red neon: (1.0, 0.1, 0.05)\r\n Blue neon: (0.1, 0.3, 1.0)\r\n Green neon: (0.1, 1.0, 0.2)\r\n Pink neon: (1.0, 0.1, 0.5)\r\n Purple neon: (0.6, 0.1, 1.0)\r\n Cyan neon: (0.0, 0.9, 1.0)\r\n \"\"\"\r\n return create_glow_material(name, color, emission_strength=strength, roughness=0.0)\r\n\r\n\r\ndef create_fire_material(\r\n name: str = \"Fire_Material\",\r\n intensity: str = \"MEDIUM\"\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Create a fire/lava emissive material.\r\n \r\n Intensity presets:\r\n EMBER: dark red-orange glow, strength 3\r\n MEDIUM: orange flame, strength 5\r\n HOT: bright yellow-white core, strength 8\r\n \r\n For best results, pair with a Point Light (energy 500, warm color).\r\n \"\"\"\r\n presets = {\r\n 'EMBER': ((0.8, 0.2, 0.0), 3.0),\r\n 'MEDIUM': ((1.0, 0.45, 0.05), 5.0),\r\n 'HOT': ((1.0, 0.75, 0.2), 8.0),\r\n }\r\n color, strength = presets.get(intensity, presets['MEDIUM'])\r\n return create_glow_material(name, color, emission_strength=strength, roughness=0.3)\r\n\r\n\r\ndef create_screen_material(\r\n name: str = \"Screen_Material\",\r\n color: tuple = (0.2, 0.5, 1.0),\r\n brightness: float = 2.0\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Create a computer/TV screen emissive material.\r\n \r\n Uses low emission strength (1–3) for a subtle screen glow.\r\n \r\n Common screen colors:\r\n Blue screen: (0.2, 0.5, 1.0) β€” idle/loading screen\r\n Green screen: (0.1, 0.8, 0.3) β€” terminal/matrix\r\n White screen: (0.9, 0.9, 0.95) β€” bright display\r\n Red alert: (0.9, 0.1, 0.05) β€” warning display\r\n \"\"\"\r\n return create_glow_material(name, color, emission_strength=brightness, roughness=0.0)\r\n\r\n\r\n# =============================================================================\r\n# COMPLETE EXAMPLE: Glowing Sun with Point Light\r\n# =============================================================================\r\n#\r\n# import bpy\r\n#\r\n# # Create sun sphere\r\n# bpy.ops.mesh.primitive_uv_sphere_add(radius=4, location=(0, 0, 0))\r\n# sun = bpy.context.active_object\r\n# sun.name = \"Sun\"\r\n# bpy.ops.object.shade_smooth()\r\n#\r\n# # Apply sun material β€” SATURATED YELLOW, strength 5 (NOT 15!)\r\n# sun_mat = bpy.data.materials.new(name=\"Sun_Material\")\r\n# bsdf = sun_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n# bsdf.inputs['Base Color'].default_value = (1.0, 0.85, 0.2, 1.0)\r\n# bsdf.inputs['Emission Color'].default_value = (1.0, 0.85, 0.2, 1.0)\r\n# bsdf.inputs['Emission Strength'].default_value = 5.0\r\n# bsdf.inputs['Roughness'].default_value = 0.0\r\n# sun.data.materials.append(sun_mat)\r\n#\r\n# # Add a point light inside for actual scene illumination\r\n# bpy.ops.object.light_add(type='POINT', location=(0, 0, 0))\r\n# light = bpy.context.active_object\r\n# light.name = \"Sun_Light\"\r\n# light.data.energy = 1500\r\n# light.data.color = (1.0, 0.92, 0.7)\r\n# light.data.shadow_soft_size = 4.0\r\n#\r\n# Result: Sun appears BRIGHT YELLOW (not white) with a warm glow in the scene.\r\n# =============================================================================","category":"utility","source_file":"emission_materials.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Export Pipeline Utilities. Functions for batch export and pipeline operations.","output":"import bpy\r\nimport os\r\n\r\n\r\ndef export_selected_fbx(\r\n filepath: str,\r\n apply_modifiers: bool = True,\r\n apply_transforms: bool = True,\r\n mesh_only: bool = False,\r\n include_armature: bool = True,\r\n use_mesh_edges: bool = False\r\n) -> bool:\r\n \"\"\"\r\n Export selected objects to FBX.\r\n \r\n Args:\r\n filepath: Output path\r\n apply_modifiers: Apply modifiers on export\r\n apply_transforms: Apply transforms\r\n mesh_only: Export only mesh data\r\n include_armature: Include armature if present\r\n use_mesh_edges: Include edge data\r\n \r\n Returns:\r\n Success status\r\n \"\"\"\r\n object_types = {'MESH'}\r\n if include_armature:\r\n object_types.add('ARMATURE')\r\n \r\n try:\r\n bpy.ops.export_scene.fbx(\r\n filepath=filepath,\r\n use_selection=True,\r\n apply_scale_options='FBX_SCALE_ALL',\r\n use_mesh_modifiers=apply_modifiers,\r\n mesh_smooth_type='FACE',\r\n use_mesh_edges=use_mesh_edges,\r\n object_types=object_types,\r\n bake_space_transform=apply_transforms\r\n )\r\n return True\r\n except:\r\n return False\r\n\r\n\r\ndef export_selected_gltf(\r\n filepath: str,\r\n format: str = 'GLB',\r\n apply_modifiers: bool = True,\r\n export_materials: str = 'EXPORT',\r\n export_animations: bool = True,\r\n export_textures: bool = True\r\n) -> bool:\r\n \"\"\"\r\n Export selected objects to glTF/GLB.\r\n \r\n Args:\r\n filepath: Output path\r\n format: 'GLB', 'GLTF_SEPARATE', 'GLTF_EMBEDDED'\r\n apply_modifiers: Apply modifiers\r\n export_materials: 'NONE', 'PLACEHOLDER', 'EXPORT'\r\n export_animations: Include animations\r\n export_textures: Include textures\r\n \r\n Returns:\r\n Success status\r\n \"\"\"\r\n try:\r\n bpy.ops.export_scene.gltf(\r\n filepath=filepath,\r\n export_format=format,\r\n use_selection=True,\r\n export_apply=apply_modifiers,\r\n export_materials=export_materials,\r\n export_animations=export_animations,\r\n export_image_format='AUTO' if export_textures else 'NONE'\r\n )\r\n return True\r\n except:\r\n return False\r\n\r\n\r\ndef export_selected_obj(\r\n filepath: str,\r\n apply_modifiers: bool = True,\r\n include_normals: bool = True,\r\n include_uvs: bool = True,\r\n include_materials: bool = True,\r\n triangulate: bool = False\r\n) -> bool:\r\n \"\"\"\r\n Export selected objects to OBJ.\r\n \r\n Returns:\r\n Success status\r\n \"\"\"\r\n try:\r\n bpy.ops.wm.obj_export(\r\n filepath=filepath,\r\n export_selected_objects=True,\r\n apply_modifiers=apply_modifiers,\r\n export_normals=include_normals,\r\n export_uv=include_uvs,\r\n export_materials=include_materials,\r\n export_triangulated_mesh=triangulate\r\n )\r\n return True\r\n except:\r\n return False\r\n\r\n\r\ndef batch_export_objects(\r\n output_dir: str,\r\n format: str = 'FBX',\r\n each_object: bool = True,\r\n naming: str = 'OBJECT'\r\n) -> list:\r\n \"\"\"\r\n Export multiple objects to individual files.\r\n \r\n Args:\r\n output_dir: Output directory\r\n format: 'FBX', 'GLTF', 'OBJ'\r\n each_object: Export each object separately\r\n naming: 'OBJECT' (use object name) or 'COLLECTION' (use collection name)\r\n \r\n Returns:\r\n List of exported file paths\r\n \"\"\"\r\n exported = []\r\n os.makedirs(output_dir, exist_ok=True)\r\n \r\n objects = bpy.context.selected_objects\r\n \r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n for obj in objects:\r\n if obj.type != 'MESH':\r\n continue\r\n \r\n obj.select_set(True)\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n # Determine filename\r\n if naming == 'COLLECTION' and obj.users_collection:\r\n name = obj.users_collection[0].name\r\n else:\r\n name = obj.name\r\n \r\n # Clean filename\r\n name = \"\".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).strip()\r\n \r\n if format == 'FBX':\r\n filepath = os.path.join(output_dir, f\"{name}.fbx\")\r\n export_selected_fbx(filepath)\r\n elif format == 'GLTF':\r\n filepath = os.path.join(output_dir, f\"{name}.glb\")\r\n export_selected_gltf(filepath)\r\n else:\r\n filepath = os.path.join(output_dir, f\"{name}.obj\")\r\n export_selected_obj(filepath)\r\n \r\n exported.append(filepath)\r\n obj.select_set(False)\r\n \r\n return exported\r\n\r\n\r\ndef prepare_for_export(\r\n obj: bpy.types.Object,\r\n apply_modifiers: bool = True,\r\n apply_transforms: bool = True,\r\n triangulate: bool = False\r\n) -> None:\r\n \"\"\"\r\n Prepare object for export.\r\n \r\n Args:\r\n obj: Object to prepare\r\n apply_modifiers: Apply all modifiers\r\n apply_transforms: Apply location/rotation/scale\r\n triangulate: Convert to triangles\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n \r\n if apply_transforms:\r\n bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)\r\n \r\n if apply_modifiers:\r\n for mod in obj.modifiers:\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=mod.name)\r\n except:\r\n pass\r\n \r\n if triangulate:\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.mesh.quads_convert_to_tris()\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef export_uv_layout(\r\n obj: bpy.types.Object,\r\n filepath: str,\r\n size: tuple = (1024, 1024),\r\n opacity: float = 0.25\r\n) -> bool:\r\n \"\"\"\r\n Export UV layout as image.\r\n \r\n Args:\r\n obj: Object with UVs\r\n filepath: Output image path\r\n size: Image dimensions\r\n opacity: Fill opacity\r\n \r\n Returns:\r\n Success status\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n \r\n try:\r\n bpy.ops.uv.export_layout(\r\n filepath=filepath,\r\n size=size,\r\n opacity=opacity,\r\n export_all=True\r\n )\r\n return True\r\n except:\r\n return False\r\n\r\n\r\ndef validate_mesh_for_export(obj: bpy.types.Object) -> dict:\r\n \"\"\"\r\n Validate mesh for common export issues.\r\n \r\n Returns:\r\n Dictionary with validation results\r\n \"\"\"\r\n issues = {\r\n 'has_ngons': False,\r\n 'has_loose_verts': False,\r\n 'has_loose_edges': False,\r\n 'has_non_manifold': False,\r\n 'missing_uvs': False,\r\n 'scale_not_applied': False\r\n }\r\n \r\n if obj.type != 'MESH':\r\n return issues\r\n \r\n mesh = obj.data\r\n \r\n # Check for ngons\r\n for poly in mesh.polygons:\r\n if len(poly.vertices) > 4:\r\n issues['has_ngons'] = True\r\n break\r\n \r\n # Check UVs\r\n if not mesh.uv_layers:\r\n issues['missing_uvs'] = True\r\n \r\n # Check scale\r\n if obj.scale != (1, 1, 1):\r\n issues['scale_not_applied'] = True\r\n \r\n # Check topology\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n \r\n bpy.ops.mesh.select_loose()\r\n if bpy.context.object.data.total_vert_sel > 0:\r\n issues['has_loose_verts'] = True\r\n \r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n bpy.ops.mesh.select_non_manifold()\r\n if bpy.context.object.data.total_vert_sel > 0:\r\n issues['has_non_manifold'] = True\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n return issues","category":"utility","source_file":"export_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Validate Mesh For Export. Category: utility","output":"import bpy\n\ndef validate_mesh_for_export(obj: bpy.types.Object) -> dict:\r\n \"\"\"\r\n Validate mesh for common export issues.\r\n \r\n Returns:\r\n Dictionary with validation results\r\n \"\"\"\r\n issues = {\r\n 'has_ngons': False,\r\n 'has_loose_verts': False,\r\n 'has_loose_edges': False,\r\n 'has_non_manifold': False,\r\n 'missing_uvs': False,\r\n 'scale_not_applied': False\r\n }\r\n \r\n if obj.type != 'MESH':\r\n return issues\r\n \r\n mesh = obj.data\r\n \r\n # Check for ngons\r\n for poly in mesh.polygons:\r\n if len(poly.vertices) > 4:\r\n issues['has_ngons'] = True\r\n break\r\n \r\n # Check UVs\r\n if not mesh.uv_layers:\r\n issues['missing_uvs'] = True\r\n \r\n # Check scale\r\n if obj.scale != (1, 1, 1):\n issues['scale_not_applied'] = True\r\n \r\n # Check topology\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n \r\n bpy.ops.mesh.select_loose()\r\n if bpy.context.object.data.total_vert_sel > 0:\r\n issues['has_loose_verts'] = True\r\n \r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n bpy.ops.mesh.select_non_manifold()\r\n if bpy.context.object.data.total_vert_sel > 0:\r\n issues['has_non_manifold'] = True\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n return issues","category":"utility","source_file":"export_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Geometry Nodes Utilities. Functions for creating and manipulating geometry node setups.","output":"import bpy\r\n\r\n\r\ndef add_geometry_nodes_modifier(\r\n obj: bpy.types.Object,\r\n name: str = \"GeometryNodes\"\r\n) -> tuple:\r\n \"\"\"\r\n Add geometry nodes modifier to object.\r\n \r\n Returns:\r\n Tuple of (modifier, node_tree)\r\n \"\"\"\r\n mod = obj.modifiers.new(name, 'NODES')\r\n \r\n if mod.node_group is None:\r\n node_group = bpy.data.node_groups.new(name, 'GeometryNodeTree')\r\n mod.node_group = node_group\r\n else:\r\n node_group = mod.node_group\r\n \r\n return mod, node_group\r\n\r\n\r\ndef setup_basic_geonodes(node_group: bpy.types.NodeTree) -> dict:\r\n \"\"\"Set up basic input/output for geometry nodes.\"\"\"\r\n nodes = node_group.nodes\r\n nodes.clear()\r\n \r\n # Group input\r\n group_input = nodes.new('NodeGroupInput')\r\n group_input.location = (-200, 0)\r\n \r\n # Group output\r\n group_output = nodes.new('NodeGroupOutput')\r\n group_output.location = (200, 0)\r\n \r\n # Add geometry socket\r\n node_group.interface.new_socket('Geometry', in_out='INPUT', socket_type='NodeSocketGeometry')\r\n node_group.interface.new_socket('Geometry', in_out='OUTPUT', socket_type='NodeSocketGeometry')\r\n \r\n # Connect\r\n node_group.links.new(group_input.outputs['Geometry'], group_output.inputs['Geometry'])\r\n \r\n return {'input': group_input, 'output': group_output}\r\n\r\n\r\ndef add_scatter_points(\r\n node_group: bpy.types.NodeTree,\r\n density: float = 10.0,\r\n seed: int = 0\r\n) -> bpy.types.Node:\r\n \"\"\"Add point distribution node.\"\"\"\r\n nodes = node_group.nodes\r\n \r\n distribute = nodes.new('GeometryNodeDistributePointsOnFaces')\r\n distribute.distribute_method = 'POISSON'\r\n distribute.inputs['Density'].default_value = density\r\n distribute.inputs['Seed'].default_value = seed\r\n \r\n return distribute\r\n\r\n\r\ndef add_instance_on_points(\r\n node_group: bpy.types.NodeTree,\r\n instance_object: bpy.types.Object = None\r\n) -> bpy.types.Node:\r\n \"\"\"Add instance on points node.\"\"\"\r\n nodes = node_group.nodes\r\n \r\n instance = nodes.new('GeometryNodeInstanceOnPoints')\r\n \r\n if instance_object:\r\n obj_info = nodes.new('GeometryNodeObjectInfo')\r\n obj_info.inputs['Object'].default_value = instance_object\r\n node_group.links.new(obj_info.outputs['Geometry'], instance.inputs['Instance'])\r\n \r\n return instance\r\n\r\n\r\ndef add_random_scale(\r\n node_group: bpy.types.NodeTree,\r\n min_scale: float = 0.5,\r\n max_scale: float = 1.5,\r\n seed: int = 0\r\n) -> bpy.types.Node:\r\n \"\"\"Add random value for scaling.\"\"\"\r\n nodes = node_group.nodes\r\n \r\n random = nodes.new('FunctionNodeRandomValue')\r\n random.data_type = 'FLOAT_VECTOR'\r\n random.inputs['Min'].default_value = (min_scale, min_scale, min_scale)\r\n random.inputs['Max'].default_value = (max_scale, max_scale, max_scale)\r\n random.inputs['Seed'].default_value = seed\r\n \r\n return random\r\n\r\n\r\ndef add_random_rotation(\r\n node_group: bpy.types.NodeTree,\r\n seed: int = 0\r\n) -> bpy.types.Node:\r\n \"\"\"Add random rotation.\"\"\"\r\n nodes = node_group.nodes\r\n \r\n random = nodes.new('FunctionNodeRandomValue')\r\n random.data_type = 'FLOAT_VECTOR'\r\n random.inputs['Min'].default_value = (0, 0, 0)\r\n random.inputs['Max'].default_value = (0, 0, 6.28319) # Z rotation only\r\n random.inputs['Seed'].default_value = seed\r\n \r\n return random\r\n\r\n\r\ndef create_scatter_setup(\r\n obj: bpy.types.Object,\r\n instance_obj: bpy.types.Object,\r\n density: float = 5.0,\r\n random_scale: bool = True,\r\n random_rotation: bool = True,\r\n name: str = \"ScatterSetup\"\r\n) -> dict:\r\n \"\"\"\r\n Create a complete scatter setup on object.\r\n \r\n Args:\r\n obj: Surface object to scatter on\r\n instance_obj: Object to instance\r\n density: Points per unit area\r\n random_scale: Add random scale variation\r\n random_rotation: Add random Z rotation\r\n name: Node group name\r\n \r\n Returns:\r\n Dictionary with modifier and nodes\r\n \"\"\"\r\n mod, node_group = add_geometry_nodes_modifier(obj, name)\r\n base = setup_basic_geonodes(node_group)\r\n nodes = node_group.nodes\r\n links = node_group.links\r\n \r\n # Distribute points\r\n distribute = add_scatter_points(node_group, density)\r\n distribute.location = (0, 100)\r\n \r\n # Instance on points\r\n instance = add_instance_on_points(node_group, instance_obj)\r\n instance.location = (200, 100)\r\n \r\n # Connect\r\n links.new(base['input'].outputs['Geometry'], distribute.inputs['Mesh'])\r\n links.new(distribute.outputs['Points'], instance.inputs['Points'])\r\n \r\n # Join geometry\r\n join = nodes.new('GeometryNodeJoinGeometry')\r\n join.location = (400, 0)\r\n \r\n links.new(base['input'].outputs['Geometry'], join.inputs['Geometry'])\r\n links.new(instance.outputs['Instances'], join.inputs['Geometry'])\r\n links.new(join.outputs['Geometry'], base['output'].inputs['Geometry'])\r\n \r\n # Random scale\r\n if random_scale:\r\n rand_scale = add_random_scale(node_group, 0.7, 1.3)\r\n rand_scale.location = (0, -100)\r\n links.new(rand_scale.outputs['Value'], instance.inputs['Scale'])\r\n \r\n # Random rotation\r\n if random_rotation:\r\n rand_rot = add_random_rotation(node_group)\r\n rand_rot.location = (0, -200)\r\n \r\n euler = nodes.new('FunctionNodeRotateEuler')\r\n euler.location = (100, -200)\r\n links.new(rand_rot.outputs['Value'], euler.inputs['Rotate By'])\r\n links.new(euler.outputs['Rotation'], instance.inputs['Rotation'])\r\n \r\n return {\r\n 'modifier': mod,\r\n 'node_group': node_group,\r\n 'distribute': distribute,\r\n 'instance': instance\r\n }","category":"utility","source_file":"geometry_nodes_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Setup Basic Geonodes. Category: utility","output":"import bpy\n\ndef setup_basic_geonodes(node_group: bpy.types.NodeTree) -> dict:\r\n \"\"\"Set up basic input/output for geometry nodes.\"\"\"\r\n nodes = node_group.nodes\r\n nodes.clear()\r\n \r\n # Group input\r\n group_input = nodes.new('NodeGroupInput')\r\n group_input.location = (-200, 0)\r\n \r\n # Group output\r\n group_output = nodes.new('NodeGroupOutput')\r\n group_output.location = (200, 0)\r\n \r\n # Add geometry socket\r\n node_group.interface.new_socket('Geometry', in_out='INPUT', socket_type='NodeSocketGeometry')\r\n node_group.interface.new_socket('Geometry', in_out='OUTPUT', socket_type='NodeSocketGeometry')\r\n \r\n # Connect\r\n node_group.links.new(group_input.outputs['Geometry'], group_output.inputs['Geometry'])\r\n \r\n return {'input': group_input, 'output': group_output}\r\n\r\n\r\ndef add_scatter_points(\r\n node_group: bpy.types.NodeTree,\r\n density: float = 10.0,\r\n seed: int = 0\r\n) -> bpy.types.Node:\r\n \"\"\"Add point distribution node.\"\"\"\r\n nodes = node_group.nodes\r\n \r\n distribute = nodes.new('GeometryNodeDistributePointsOnFaces')\r\n distribute.distribute_method = 'POISSON'\r\n distribute.inputs['Density'].default_value = density\r\n distribute.inputs['Seed'].default_value = seed\r\n \r\n return distribute\r\n\r\n\r\ndef add_instance_on_points(\r\n node_group: bpy.types.NodeTree,\r\n instance_object: bpy.types.Object = None\r\n) -> bpy.types.Node:\r\n \"\"\"Add instance on points node.\"\"\"\r\n nodes = node_group.nodes\r\n \r\n instance = nodes.new('GeometryNodeInstanceOnPoints')\r\n \r\n if instance_object:\r\n obj_info = nodes.new('GeometryNodeObjectInfo')\r\n obj_info.inputs['Object'].default_value = instance_object\r\n node_group.links.new(obj_info.outputs['Geometry'], instance.inputs['Instance'])\r\n \r\n return instance\r\n\r\n\r\ndef add_random_scale(\r\n node_group: bpy.types.NodeTree,\r\n min_scale: float = 0.5,\r\n max_scale: float = 1.5,\r\n seed: int = 0\r\n) -> bpy.types.Node:\r\n \"\"\"Add random value for scaling.\"\"\"\r\n nodes = node_group.nodes\r\n \r\n random = nodes.new('FunctionNodeRandomValue')\r\n random.data_type = 'FLOAT_VECTOR'\r\n random.inputs['Min'].default_value = (min_scale, min_scale, min_scale)\r\n random.inputs['Max'].default_value = (max_scale, max_scale, max_scale)\r\n random.inputs['Seed'].default_value = seed\r\n \r\n return random\r\n\r\n\r\ndef add_random_rotation(\r\n node_group: bpy.types.NodeTree,\r\n seed: int = 0\r\n) -> bpy.types.Node:\r\n \"\"\"Add random rotation.\"\"\"\r\n nodes = node_group.nodes\r\n \r\n random = nodes.new('FunctionNodeRandomValue')\r\n random.data_type = 'FLOAT_VECTOR'\r\n random.inputs['Min'].default_value = (0, 0, 0)\r\n random.inputs['Max'].default_value = (0, 0, 6.28319) # Z rotation only\r\n random.inputs['Seed'].default_value = seed\r\n \r\n return random\r\n\r\n\r\ndef create_scatter_setup(\r\n obj: bpy.types.Object,\r\n instance_obj: bpy.types.Object,\r\n density: float = 5.0,\r\n random_scale: bool = True,\r\n random_rotation: bool = True,\r\n name: str = \"ScatterSetup\"\r\n) -> dict:\r\n \"\"\"\r\n Create a complete scatter setup on object.\r\n \r\n Args:\r\n obj: Surface object to scatter on\r\n instance_obj: Object to instance\r\n density: Points per unit area\r\n random_scale: Add random scale variation\r\n random_rotation: Add random Z rotation\r\n name: Node group name\r\n \r\n Returns:\r\n Dictionary with modifier and nodes\r\n \"\"\"\r\n mod, node_group = add_geometry_nodes_modifier(obj, name)\r\n base = setup_basic_geonodes(node_group)\r\n nodes = node_group.nodes\r\n links = node_group.links\r\n \r\n # Distribute points\r\n distribute = add_scatter_points(node_group, density)\r\n distribute.location = (0, 100)\r\n \r\n # Instance on points\r\n instance = add_instance_on_points(node_group, instance_obj)\r\n instance.location = (200, 100)\r\n \r\n # Connect\r\n links.new(base['input'].outputs['Geometry'], distribute.inputs['Mesh'])\r\n links.new(distribute.outputs['Points'], instance.inputs['Points'])\r\n \r\n # Join geometry\r\n join = nodes.new('GeometryNodeJoinGeometry')\r\n join.location = (400, 0)\r\n \r\n links.new(base['input'].outputs['Geometry'], join.inputs['Geometry'])\r\n links.new(instance.outputs['Instances'], join.inputs['Geometry'])\r\n links.new(join.outputs['Geometry'], base['output'].inputs['Geometry'])\r\n \r\n # Random scale\r\n if random_scale:\r\n rand_scale = add_random_scale(node_group, 0.7, 1.3)\r\n rand_scale.location = (0, -100)\r\n links.new(rand_scale.outputs['Value'], instance.inputs['Scale'])\r\n \r\n # Random rotation\r\n if random_rotation:\r\n rand_rot = add_random_rotation(node_group)\r\n rand_rot.location = (0, -200)\r\n \r\n euler = nodes.new('FunctionNodeRotateEuler')\r\n euler.location = (100, -200)\r\n links.new(rand_rot.outputs['Value'], euler.inputs['Rotate By'])\r\n links.new(euler.outputs['Rotation'], instance.inputs['Rotation'])\r\n \r\n return {\r\n 'modifier': mod,\r\n 'node_group': node_group,\r\n 'distribute': distribute,\r\n 'instance': instance\r\n }","category":"utility","source_file":"geometry_nodes_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Geometry Nodes Utilities. Functions for creating and managing Geometry Nodes.","output":"import bpy\r\n\r\n\r\ndef add_geometry_nodes_modifier(\r\n obj: bpy.types.Object,\r\n name: str = \"GeometryNodes\"\r\n) -> tuple:\r\n \"\"\"\r\n Add Geometry Nodes modifier to object.\r\n \r\n Args:\r\n obj: Target object\r\n name: Modifier name\r\n \r\n Returns:\r\n Tuple of (modifier, node_group)\r\n \"\"\"\r\n mod = obj.modifiers.new(name, 'NODES')\r\n \r\n # Create node group\r\n group = bpy.data.node_groups.new(name, 'GeometryNodeTree')\r\n mod.node_group = group\r\n \r\n # Add input/output nodes\r\n group_input = group.nodes.new('NodeGroupInput')\r\n group_input.location = (-200, 0)\r\n \r\n group_output = group.nodes.new('NodeGroupOutput')\r\n group_output.location = (200, 0)\r\n \r\n # Add geometry socket\r\n group.interface.new_socket('Geometry', in_out='INPUT', socket_type='NodeSocketGeometry')\r\n group.interface.new_socket('Geometry', in_out='OUTPUT', socket_type='NodeSocketGeometry')\r\n \r\n # Connect input to output\r\n group.links.new(group_input.outputs['Geometry'], group_output.inputs['Geometry'])\r\n \r\n return mod, group\r\n\r\n\r\ndef add_gn_node(\r\n group: bpy.types.NodeTree,\r\n node_type: str,\r\n location: tuple = (0, 0),\r\n name: str = None\r\n) -> bpy.types.Node:\r\n \"\"\"\r\n Add node to geometry node tree.\r\n \r\n Args:\r\n group: Node group\r\n node_type: Node type (e.g., 'GeometryNodeMeshPrimitiveCube')\r\n location: Node position\r\n name: Optional node name\r\n \r\n Returns:\r\n Created node\r\n \"\"\"\r\n node = group.nodes.new(node_type)\r\n node.location = location\r\n if name:\r\n node.name = name\r\n return node\r\n\r\n\r\ndef connect_gn_nodes(\r\n group: bpy.types.NodeTree,\r\n from_node: str,\r\n from_socket: str,\r\n to_node: str,\r\n to_socket: str\r\n) -> None:\r\n \"\"\"Connect two geometry nodes.\"\"\"\r\n source = group.nodes.get(from_node)\r\n dest = group.nodes.get(to_node)\r\n \r\n if source and dest:\r\n group.links.new(source.outputs[from_socket], dest.inputs[to_socket])\r\n\r\n\r\ndef add_gn_input(\r\n group: bpy.types.NodeTree,\r\n name: str,\r\n socket_type: str,\r\n default_value = None\r\n) -> None:\r\n \"\"\"\r\n Add input socket to geometry node group.\r\n \r\n Args:\r\n group: Node group\r\n name: Input name\r\n socket_type: 'NodeSocketFloat', 'NodeSocketVector', 'NodeSocketInt', etc.\r\n default_value: Optional default value\r\n \"\"\"\r\n socket = group.interface.new_socket(name, in_out='INPUT', socket_type=socket_type)\r\n if default_value is not None:\r\n socket.default_value = default_value\r\n\r\n\r\ndef add_gn_scatter_setup(\r\n obj: bpy.types.Object,\r\n density: float = 10.0,\r\n scale: float = 1.0,\r\n seed: int = 0\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Add point scatter setup to object.\r\n \r\n Args:\r\n obj: Target object\r\n density: Points per unit area\r\n scale: Instance scale\r\n seed: Random seed\r\n \r\n Returns:\r\n The geometry nodes modifier\r\n \"\"\"\r\n mod, group = add_geometry_nodes_modifier(obj, \"Scatter\")\r\n \r\n # Get input/output\r\n group_input = group.nodes.get('Group Input')\r\n group_output = group.nodes.get('Group Output')\r\n \r\n # Distribute points\r\n distribute = add_gn_node(group, 'GeometryNodeDistributePointsOnFaces', (-100, 0))\r\n distribute.distribute_method = 'RANDOM'\r\n distribute.inputs['Density'].default_value = density\r\n distribute.inputs['Seed'].default_value = seed\r\n \r\n # Instance on points\r\n instance = add_gn_node(group, 'GeometryNodeInstanceOnPoints', (100, 0))\r\n \r\n # Join geometry\r\n join = add_gn_node(group, 'GeometryNodeJoinGeometry', (300, 0))\r\n \r\n # Connect\r\n group.links.new(group_input.outputs['Geometry'], distribute.inputs['Mesh'])\r\n group.links.new(distribute.outputs['Points'], instance.inputs['Points'])\r\n group.links.new(group_input.outputs['Geometry'], join.inputs['Geometry'])\r\n group.links.new(instance.outputs['Instances'], join.inputs['Geometry'])\r\n group.links.new(join.outputs['Geometry'], group_output.inputs['Geometry'])\r\n \r\n # Add instance input\r\n add_gn_input(group, 'Instance', 'NodeSocketGeometry')\r\n \r\n return mod\r\n\r\n\r\ndef add_gn_extrude_setup(\r\n obj: bpy.types.Object,\r\n height: float = 0.5,\r\n individual: bool = False\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Add extrude along normals setup.\r\n \r\n Args:\r\n obj: Target object\r\n height: Extrude height\r\n individual: Extrude faces individually\r\n \r\n Returns:\r\n The geometry nodes modifier\r\n \"\"\"\r\n mod, group = add_geometry_nodes_modifier(obj, \"Extrude\")\r\n \r\n group_input = group.nodes.get('Group Input')\r\n group_output = group.nodes.get('Group Output')\r\n \r\n # Extrude mesh\r\n extrude = add_gn_node(group, 'GeometryNodeExtrudeMesh', (0, 0))\r\n extrude.mode = 'FACES'\r\n extrude.inputs['Offset Scale'].default_value = height\r\n extrude.inputs['Individual'].default_value = individual\r\n \r\n group.links.new(group_input.outputs['Geometry'], extrude.inputs['Mesh'])\r\n group.links.new(extrude.outputs['Mesh'], group_output.inputs['Geometry'])\r\n \r\n # Add height input\r\n add_gn_input(group, 'Height', 'NodeSocketFloat', height)\r\n \r\n return mod\r\n\r\n\r\ndef add_gn_array_setup(\r\n obj: bpy.types.Object,\r\n count: int = 5,\r\n offset: tuple = (1, 0, 0)\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Add linear array setup.\r\n \r\n Args:\r\n obj: Target object\r\n count: Number of instances\r\n offset: Offset between instances\r\n \r\n Returns:\r\n The geometry nodes modifier\r\n \"\"\"\r\n mod, group = add_geometry_nodes_modifier(obj, \"Array\")\r\n \r\n group_input = group.nodes.get('Group Input')\r\n group_output = group.nodes.get('Group Output')\r\n \r\n # Mesh to points line\r\n mesh_line = add_gn_node(group, 'GeometryNodeMeshLine', (-100, -100))\r\n mesh_line.mode = 'OFFSET'\r\n mesh_line.inputs['Count'].default_value = count\r\n mesh_line.inputs['Offset'].default_value = offset\r\n \r\n # Instance on points\r\n instance = add_gn_node(group, 'GeometryNodeInstanceOnPoints', (100, 0))\r\n \r\n # Realize instances\r\n realize = add_gn_node(group, 'GeometryNodeRealizeInstances', (300, 0))\r\n \r\n # Connect\r\n group.links.new(mesh_line.outputs['Mesh'], instance.inputs['Points'])\r\n group.links.new(group_input.outputs['Geometry'], instance.inputs['Instance'])\r\n group.links.new(instance.outputs['Instances'], realize.inputs['Geometry'])\r\n group.links.new(realize.outputs['Geometry'], group_output.inputs['Geometry'])\r\n \r\n return mod\r\n\r\n\r\ndef apply_geometry_nodes(obj: bpy.types.Object, modifier_name: str = None) -> bool:\r\n \"\"\"Apply geometry nodes modifier.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n if modifier_name:\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=modifier_name)\r\n return True\r\n except:\r\n return False\r\n else:\r\n # Apply first geometry nodes modifier\r\n for mod in obj.modifiers:\r\n if mod.type == 'NODES':\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=mod.name)\r\n return True\r\n except:\r\n return False\r\n \r\n return False","category":"utility","source_file":"geonodes_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Apply Geometry Nodes. Category: utility","output":"import bpy\n\ndef apply_geometry_nodes(obj: bpy.types.Object, modifier_name: str = None) -> bool:\r\n \"\"\"Apply geometry nodes modifier.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n if modifier_name:\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=modifier_name)\r\n return True\r\n except:\r\n return False\r\n else:\r\n # Apply first geometry nodes modifier\r\n for mod in obj.modifiers:\r\n if mod.type == 'NODES':\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=mod.name)\r\n return True\r\n except:\r\n return False\r\n \r\n return False","category":"utility","source_file":"geonodes_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Grease Pencil Utilities. Functions for Grease Pencil object creation and editing.","output":"import bpy\r\n\r\n\r\ndef create_grease_pencil_object(\r\n name: str = \"GPencil\",\r\n location: tuple = (0, 0, 0)\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create new Grease Pencil object.\r\n \r\n Args:\r\n name: Object name\r\n location: Position\r\n \r\n Returns:\r\n The Grease Pencil object\r\n \"\"\"\r\n gpd = bpy.data.grease_pencils.new(name)\r\n gpo = bpy.data.objects.new(name, gpd)\r\n bpy.context.collection.objects.link(gpo)\r\n gpo.location = location\r\n \r\n # Add default layer\r\n layer = gpd.layers.new(\"Layer\")\r\n layer.frames.new(1)\r\n \r\n return gpo\r\n\r\n\r\ndef add_gp_layer(\r\n gp_object: bpy.types.Object,\r\n name: str = \"Layer\",\r\n color: tuple = (0, 0, 0, 1)\r\n) -> bpy.types.GPencilLayer:\r\n \"\"\"\r\n Add layer to Grease Pencil object.\r\n \r\n Args:\r\n gp_object: Grease Pencil object\r\n name: Layer name\r\n color: RGBA layer tint\r\n \r\n Returns:\r\n The created layer\r\n \"\"\"\r\n gpd = gp_object.data\r\n layer = gpd.layers.new(name)\r\n layer.tint_color = color[:3]\r\n layer.tint_factor = color[3] if len(color) > 3 else 1.0\r\n layer.frames.new(1)\r\n \r\n return layer\r\n\r\n\r\ndef add_gp_material(\r\n gp_object: bpy.types.Object,\r\n name: str = \"GPMaterial\",\r\n color: tuple = (0, 0, 0, 1),\r\n fill_color: tuple = None,\r\n stroke_style: str = 'SOLID'\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Add material to Grease Pencil object.\r\n \r\n Args:\r\n gp_object: Grease Pencil object\r\n name: Material name\r\n color: Stroke color (RGBA)\r\n fill_color: Fill color (None for no fill)\r\n stroke_style: 'SOLID', 'DOTS', 'SQUARES'\r\n \r\n Returns:\r\n The created material\r\n \"\"\"\r\n mat = bpy.data.materials.new(name)\r\n bpy.data.materials.create_gpencil_data(mat)\r\n \r\n mat.grease_pencil.color = color\r\n mat.grease_pencil.stroke_style = stroke_style\r\n \r\n if fill_color:\r\n mat.grease_pencil.show_fill = True\r\n mat.grease_pencil.fill_color = fill_color\r\n else:\r\n mat.grease_pencil.show_fill = False\r\n \r\n gp_object.data.materials.append(mat)\r\n \r\n return mat\r\n\r\n\r\ndef set_gp_brush(brush_name: str) -> None:\r\n \"\"\"\r\n Set Grease Pencil brush.\r\n \r\n Args:\r\n brush_name: Brush name (e.g., 'Pencil', 'Ink Pen', 'Marker')\r\n \"\"\"\r\n brush = bpy.data.brushes.get(brush_name)\r\n if brush:\r\n bpy.context.tool_settings.gpencil_paint.brush = brush\r\n\r\n\r\ndef configure_gp_brush(\r\n size: int = 50,\r\n strength: float = 1.0,\r\n use_pressure: bool = True\r\n) -> None:\r\n \"\"\"Configure active Grease Pencil brush.\"\"\"\r\n brush = bpy.context.tool_settings.gpencil_paint.brush\r\n if brush:\r\n brush.size = size\r\n brush.gpencil_settings.pen_strength = strength\r\n brush.gpencil_settings.use_pressure = use_pressure\r\n\r\n\r\ndef enable_gp_draw_mode(gp_object: bpy.types.Object) -> None:\r\n \"\"\"Enter Grease Pencil draw mode.\"\"\"\r\n bpy.context.view_layer.objects.active = gp_object\r\n bpy.ops.object.mode_set(mode='PAINT_GPENCIL')\r\n\r\n\r\ndef enable_gp_edit_mode(gp_object: bpy.types.Object) -> None:\r\n \"\"\"Enter Grease Pencil edit mode.\"\"\"\r\n bpy.context.view_layer.objects.active = gp_object\r\n bpy.ops.object.mode_set(mode='EDIT_GPENCIL')\r\n\r\n\r\ndef enable_gp_sculpt_mode(gp_object: bpy.types.Object) -> None:\r\n \"\"\"Enter Grease Pencil sculpt mode.\"\"\"\r\n bpy.context.view_layer.objects.active = gp_object\r\n bpy.ops.object.mode_set(mode='SCULPT_GPENCIL')\r\n\r\n\r\ndef add_gp_modifier(\r\n gp_object: bpy.types.Object,\r\n modifier_type: str,\r\n name: str = None\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"\r\n Add Grease Pencil modifier.\r\n \r\n Args:\r\n gp_object: Grease Pencil object\r\n modifier_type: 'GP_SMOOTH', 'GP_SUBDIV', 'GP_THICK', 'GP_TINT', etc.\r\n name: Optional modifier name\r\n \r\n Returns:\r\n The created modifier\r\n \"\"\"\r\n mod_name = name or modifier_type\r\n mod = gp_object.grease_pencil_modifiers.new(mod_name, modifier_type)\r\n return mod\r\n\r\n\r\ndef add_gp_smooth(\r\n gp_object: bpy.types.Object,\r\n factor: float = 0.5,\r\n step: int = 1\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"Add smoothing modifier.\"\"\"\r\n mod = add_gp_modifier(gp_object, 'GP_SMOOTH', 'Smooth')\r\n mod.factor = factor\r\n mod.step = step\r\n return mod\r\n\r\n\r\ndef add_gp_thickness(\r\n gp_object: bpy.types.Object,\r\n thickness: int = 10,\r\n normalize: bool = False\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"Add thickness modifier.\"\"\"\r\n mod = add_gp_modifier(gp_object, 'GP_THICK', 'Thickness')\r\n mod.thickness = thickness\r\n mod.normalize_thickness = normalize\r\n return mod\r\n\r\n\r\ndef add_gp_tint(\r\n gp_object: bpy.types.Object,\r\n color: tuple = (1, 0, 0),\r\n factor: float = 0.5\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"Add tint modifier.\"\"\"\r\n mod = add_gp_modifier(gp_object, 'GP_TINT', 'Tint')\r\n mod.color = color\r\n mod.factor = factor\r\n return mod\r\n\r\n\r\ndef add_gp_lineart(\r\n gp_object: bpy.types.Object,\r\n source_object: bpy.types.Object = None,\r\n source_collection: bpy.types.Collection = None\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"\r\n Add Line Art modifier for automatic line generation.\r\n \r\n Args:\r\n gp_object: Target Grease Pencil object\r\n source_object: Single source object\r\n source_collection: Collection of source objects\r\n \r\n Returns:\r\n The Line Art modifier\r\n \"\"\"\r\n mod = add_gp_modifier(gp_object, 'GP_LINEART', 'LineArt')\r\n \r\n if source_object:\r\n mod.source_type = 'OBJECT'\r\n mod.source_object = source_object\r\n elif source_collection:\r\n mod.source_type = 'COLLECTION'\r\n mod.source_collection = source_collection\r\n else:\r\n mod.source_type = 'SCENE'\r\n \r\n return mod\r\n\r\n\r\ndef convert_gp_to_curves(gp_object: bpy.types.Object) -> bpy.types.Object:\r\n \"\"\"Convert Grease Pencil strokes to curves.\"\"\"\r\n bpy.context.view_layer.objects.active = gp_object\r\n gp_object.select_set(True)\r\n \r\n bpy.ops.gpencil.convert(type='CURVE')\r\n \r\n return bpy.context.active_object\r\n\r\n\r\ndef set_gp_onion_skin(\r\n gp_object: bpy.types.Object,\r\n enabled: bool = True,\r\n frames_before: int = 1,\r\n frames_after: int = 1\r\n) -> None:\r\n \"\"\"Configure onion skinning.\"\"\"\r\n gpd = gp_object.data\r\n gpd.use_onion_skinning = enabled\r\n gpd.onion_factor = 0.5\r\n \r\n if enabled:\r\n gpd.ghost_before_range = frames_before\r\n gpd.ghost_after_range = frames_after","category":"utility","source_file":"grease_pencil_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Set Gp Brush. Category: utility","output":"import bpy\n\ndef set_gp_brush(brush_name: str) -> None:\r\n \"\"\"\r\n Set Grease Pencil brush.\r\n \r\n Args:\r\n brush_name: Brush name (e.g., 'Pencil', 'Ink Pen', 'Marker')\r\n \"\"\"\r\n brush = bpy.data.brushes.get(brush_name)\r\n if brush:\r\n bpy.context.tool_settings.gpencil_paint.brush = brush\r\n\r\n\r\ndef configure_gp_brush(\r\n size: int = 50,\r\n strength: float = 1.0,\r\n use_pressure: bool = True\r\n) -> None:\r\n \"\"\"Configure active Grease Pencil brush.\"\"\"\r\n brush = bpy.context.tool_settings.gpencil_paint.brush\r\n if brush:\r\n brush.size = size\r\n brush.gpencil_settings.pen_strength = strength\r\n brush.gpencil_settings.use_pressure = use_pressure\r\n\r\n\r\ndef enable_gp_draw_mode(gp_object: bpy.types.Object) -> None:\r\n \"\"\"Enter Grease Pencil draw mode.\"\"\"\r\n bpy.context.view_layer.objects.active = gp_object\r\n bpy.ops.object.mode_set(mode='PAINT_GPENCIL')\r\n\r\n\r\ndef enable_gp_edit_mode(gp_object: bpy.types.Object) -> None:\r\n \"\"\"Enter Grease Pencil edit mode.\"\"\"\r\n bpy.context.view_layer.objects.active = gp_object\r\n bpy.ops.object.mode_set(mode='EDIT_GPENCIL')\r\n\r\n\r\ndef enable_gp_sculpt_mode(gp_object: bpy.types.Object) -> None:\r\n \"\"\"Enter Grease Pencil sculpt mode.\"\"\"\r\n bpy.context.view_layer.objects.active = gp_object\r\n bpy.ops.object.mode_set(mode='SCULPT_GPENCIL')\r\n\r\n\r\ndef add_gp_modifier(\r\n gp_object: bpy.types.Object,\r\n modifier_type: str,\r\n name: str = None\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"\r\n Add Grease Pencil modifier.\r\n \r\n Args:\r\n gp_object: Grease Pencil object\r\n modifier_type: 'GP_SMOOTH', 'GP_SUBDIV', 'GP_THICK', 'GP_TINT', etc.\r\n name: Optional modifier name\r\n \r\n Returns:\r\n The created modifier\r\n \"\"\"\r\n mod_name = name or modifier_type\r\n mod = gp_object.grease_pencil_modifiers.new(mod_name, modifier_type)\r\n return mod\r\n\r\n\r\ndef add_gp_smooth(\r\n gp_object: bpy.types.Object,\r\n factor: float = 0.5,\r\n step: int = 1\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"Add smoothing modifier.\"\"\"\r\n mod = add_gp_modifier(gp_object, 'GP_SMOOTH', 'Smooth')\r\n mod.factor = factor\r\n mod.step = step\r\n return mod\r\n\r\n\r\ndef add_gp_thickness(\r\n gp_object: bpy.types.Object,\r\n thickness: int = 10,\r\n normalize: bool = False\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"Add thickness modifier.\"\"\"\r\n mod = add_gp_modifier(gp_object, 'GP_THICK', 'Thickness')\r\n mod.thickness = thickness\r\n mod.normalize_thickness = normalize\r\n return mod\r\n\r\n\r\ndef add_gp_tint(\r\n gp_object: bpy.types.Object,\r\n color: tuple = (1, 0, 0),\r\n factor: float = 0.5\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"Add tint modifier.\"\"\"\r\n mod = add_gp_modifier(gp_object, 'GP_TINT', 'Tint')\r\n mod.color = color\r\n mod.factor = factor\r\n return mod\r\n\r\n\r\ndef add_gp_lineart(\r\n gp_object: bpy.types.Object,\r\n source_object: bpy.types.Object = None,\r\n source_collection: bpy.types.Collection = None\r\n) -> bpy.types.GpencilModifier:\r\n \"\"\"\r\n Add Line Art modifier for automatic line generation.\r\n \r\n Args:\r\n gp_object: Target Grease Pencil object\r\n source_object: Single source object\r\n source_collection: Collection of source objects\r\n \r\n Returns:\r\n The Line Art modifier\r\n \"\"\"\r\n mod = add_gp_modifier(gp_object, 'GP_LINEART', 'LineArt')\r\n \r\n if source_object:\r\n mod.source_type = 'OBJECT'\r\n mod.source_object = source_object\r\n elif source_collection:\r\n mod.source_type = 'COLLECTION'\r\n mod.source_collection = source_collection\r\n else:\r\n mod.source_type = 'SCENE'\r\n \r\n return mod\r\n\r\n\r\ndef convert_gp_to_curves(gp_object: bpy.types.Object) -> bpy.types.Object:\r\n \"\"\"Convert Grease Pencil strokes to curves.\"\"\"\r\n bpy.context.view_layer.objects.active = gp_object\r\n gp_object.select_set(True)\r\n \r\n bpy.ops.gpencil.convert(type='CURVE')\r\n \r\n return bpy.context.active_object\r\n\r\n\r\ndef set_gp_onion_skin(\r\n gp_object: bpy.types.Object,\r\n enabled: bool = True,\r\n frames_before: int = 1,\r\n frames_after: int = 1\r\n) -> None:\r\n \"\"\"Configure onion skinning.\"\"\"\r\n gpd = gp_object.data\r\n gpd.use_onion_skinning = enabled\r\n gpd.onion_factor = 0.5\r\n \r\n if enabled:\r\n gpd.ghost_before_range = frames_before\r\n gpd.ghost_after_range = frames_after","category":"utility","source_file":"grease_pencil_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: HDRI Environment & ImageBased Lighting β€” Blender 4.0/5.0. HDRI Environment & Image-Based Lighting β€” Blender 4.0/5.0 HDRI (High Dynamic Range Image) environment textures for realistic image-based lighting. Covers:","output":"import bpy\r\nimport math\r\n\r\n\r\ndef setup_hdri_world(hdri_path, strength=1.0, rotation_z=0.0):\r\n \"\"\"Set up HDRI environment lighting for the scene.\r\n\r\n This is the standard pattern for image-based lighting in Blender.\r\n Creates: Tex Coord β†’ Mapping β†’ Environment Texture β†’ Background β†’ Output\r\n\r\n Args:\r\n hdri_path: Path to .hdr/.exr file (absolute or relative //)\r\n strength: Background light intensity (1.0 = natural, 2.0 = bright)\r\n rotation_z: Z rotation in degrees for rotating the environment\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n node_tree = world.node_tree\r\n nodes = node_tree.nodes\r\n links = node_tree.links\r\n nodes.clear()\r\n\r\n # Texture Coordinate β†’ Mapping (for rotation control)\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-600, 0)\r\n\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-400, 0)\r\n mapping.inputs['Rotation'].default_value[2] = math.radians(rotation_z)\r\n\r\n # Environment Texture β€” NOT ShaderNodeTexImage!\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.location = (-200, 0)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n # HDR files default to Linear color space β€” do NOT change to Non-Color\r\n\r\n # Background shader\r\n background = nodes.new('ShaderNodeBackground')\r\n background.location = (100, 0)\r\n background.inputs['Strength'].default_value = strength\r\n\r\n # World Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (300, 0)\r\n\r\n # Link chain\r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n\r\n return world\r\n\r\n\r\ndef setup_solid_color_world(color=(0.05, 0.05, 0.1), strength=1.0):\r\n \"\"\"Set up a solid color world background (no HDRI needed).\r\n\r\n Useful for studio-style renders or when you want full lighting control.\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n node_tree = world.node_tree\r\n nodes = node_tree.nodes\r\n links = node_tree.links\r\n nodes.clear()\r\n\r\n background = nodes.new('ShaderNodeBackground')\r\n background.location = (0, 0)\r\n background.inputs['Color'].default_value = (*color, 1.0)\r\n background.inputs['Strength'].default_value = strength\r\n\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n return world\r\n\r\n\r\ndef setup_gradient_sky_world(sky_color=(0.4, 0.6, 0.9),\r\n horizon_color=(0.85, 0.85, 0.9),\r\n strength=1.0):\r\n \"\"\"Create a gradient sky from horizon to zenith (no HDRI file needed).\r\n\r\n Uses ColorRamp + Texture Coordinate to create a natural sky gradient.\r\n Good for outdoor scenes without a specific HDRI.\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n node_tree = world.node_tree\r\n nodes = node_tree.nodes\r\n links = node_tree.links\r\n nodes.clear()\r\n\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-600, 0)\r\n\r\n separate = nodes.new('ShaderNodeSeparateXYZ')\r\n separate.location = (-400, 0)\r\n\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-200, 0)\r\n ramp.color_ramp.elements[0].position = 0.4\r\n ramp.color_ramp.elements[0].color = (*horizon_color, 1.0)\r\n ramp.color_ramp.elements[1].position = 0.6\r\n ramp.color_ramp.elements[1].color = (*sky_color, 1.0)\r\n\r\n background = nodes.new('ShaderNodeBackground')\r\n background.location = (100, 0)\r\n background.inputs['Strength'].default_value = strength\r\n\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (300, 0)\r\n\r\n links.new(tex_coord.outputs['Generated'], separate.inputs['Vector'])\r\n links.new(separate.outputs['Z'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n\r\n return world","category":"utility","source_file":"hdri_lighting.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Set up HDRI environment lighting for the scene.\r\n\r\n This is the standard pattern for image-based lighting in Blender.\r\n Creates: Tex Coord β†’ Mapping β†’ Environment Texture β†’ Background β†’ Output\r\n\r\n Args:\r\n hdri_path: Path to .hdr/.exr file (absolute or relative //)\r\n strength: Background light intensity (1.0 = natural, 2.0 = bright)\r\n rotation_z: Z rotation in degrees for rotating the environment","output":"import bpy\n\ndef setup_hdri_world(hdri_path, strength=1.0, rotation_z=0.0):\n \"\"\"Set up HDRI environment lighting for the scene.\r\n\r\n This is the standard pattern for image-based lighting in Blender.\r\n Creates: Tex Coord β†’ Mapping β†’ Environment Texture β†’ Background β†’ Output\r\n\r\n Args:\r\n hdri_path: Path to .hdr/.exr file (absolute or relative //)\r\n strength: Background light intensity (1.0 = natural, 2.0 = bright)\r\n rotation_z: Z rotation in degrees for rotating the environment\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n node_tree = world.node_tree\r\n nodes = node_tree.nodes\r\n links = node_tree.links\r\n nodes.clear()\r\n\r\n # Texture Coordinate β†’ Mapping (for rotation control)\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-600, 0)\r\n\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-400, 0)\r\n mapping.inputs['Rotation'].default_value[2] = math.radians(rotation_z)\r\n\r\n # Environment Texture β€” NOT ShaderNodeTexImage!\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.location = (-200, 0)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n # HDR files default to Linear color space β€” do NOT change to Non-Color\r\n\r\n # Background shader\r\n background = nodes.new('ShaderNodeBackground')\r\n background.location = (100, 0)\r\n background.inputs['Strength'].default_value = strength\r\n\r\n # World Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (300, 0)\r\n\r\n # Link chain\r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n\r\n return world","category":"utility","source_file":"hdri_lighting.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Set up a solid color world background (no HDRI needed).\r\n\r\n Useful for studio-style renders or when you want full lighting control.","output":"import bpy\n\ndef setup_solid_color_world(color=(0.05, 0.05, 0.1), strength=1.0):\n \"\"\"Set up a solid color world background (no HDRI needed).\r\n\r\n Useful for studio-style renders or when you want full lighting control.\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n node_tree = world.node_tree\r\n nodes = node_tree.nodes\r\n links = node_tree.links\r\n nodes.clear()\r\n\r\n background = nodes.new('ShaderNodeBackground')\r\n background.location = (0, 0)\r\n background.inputs['Color'].default_value = (*color, 1.0)\r\n background.inputs['Strength'].default_value = strength\r\n\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n return world","category":"utility","source_file":"hdri_lighting.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Image and Texture Utilities. Functions for loading, creating, and managing images.","output":"import bpy\r\nimport os\r\n\r\n\r\ndef load_image(\r\n filepath: str,\r\n check_existing: bool = True\r\n) -> bpy.types.Image:\r\n \"\"\"\r\n Load image from file.\r\n \r\n Args:\r\n filepath: Path to image file\r\n check_existing: Reuse if already loaded\r\n \r\n Returns:\r\n The loaded image\r\n \"\"\"\r\n return bpy.data.images.load(filepath, check_existing=check_existing)\r\n\r\n\r\ndef create_image(\r\n name: str,\r\n width: int = 1024,\r\n height: int = 1024,\r\n color: tuple = (0, 0, 0, 1),\r\n alpha: bool = True,\r\n float_buffer: bool = False\r\n) -> bpy.types.Image:\r\n \"\"\"\r\n Create new image.\r\n \r\n Args:\r\n name: Image name\r\n width: Width in pixels\r\n height: Height in pixels\r\n color: Fill color (RGBA)\r\n alpha: Include alpha channel\r\n float_buffer: Use 32-bit float\r\n \r\n Returns:\r\n The created image\r\n \"\"\"\r\n img = bpy.data.images.new(\r\n name,\r\n width=width,\r\n height=height,\r\n alpha=alpha,\r\n float_buffer=float_buffer\r\n )\r\n \r\n # Fill with color\r\n pixels = list(color) * (width * height)\r\n img.pixels = pixels\r\n \r\n return img\r\n\r\n\r\ndef save_image(\r\n image: bpy.types.Image,\r\n filepath: str,\r\n file_format: str = 'PNG'\r\n) -> bool:\r\n \"\"\"\r\n Save image to file.\r\n \r\n Args:\r\n image: Image to save\r\n filepath: Output path\r\n file_format: 'PNG', 'JPEG', 'TIFF', 'OPEN_EXR', 'TARGA'\r\n \r\n Returns:\r\n Success status\r\n \"\"\"\r\n try:\r\n image.filepath_raw = filepath\r\n image.file_format = file_format\r\n image.save()\r\n return True\r\n except:\r\n return False\r\n\r\n\r\ndef pack_image(image: bpy.types.Image) -> None:\r\n \"\"\"Pack image into blend file.\"\"\"\r\n if not image.packed_file:\r\n image.pack()\r\n\r\n\r\ndef unpack_image(\r\n image: bpy.types.Image,\r\n method: str = 'USE_ORIGINAL'\r\n) -> None:\r\n \"\"\"\r\n Unpack image to external file.\r\n \r\n Args:\r\n method: 'USE_ORIGINAL', 'WRITE_ORIGINAL', 'USE_LOCAL', 'WRITE_LOCAL'\r\n \"\"\"\r\n if image.packed_file:\r\n image.unpack(method=method)\r\n\r\n\r\ndef reload_image(image: bpy.types.Image) -> None:\r\n \"\"\"Reload image from disk.\"\"\"\r\n image.reload()\r\n\r\n\r\ndef get_image(name: str) -> bpy.types.Image:\r\n \"\"\"Get image by name.\"\"\"\r\n return bpy.data.images.get(name)\r\n\r\n\r\ndef set_color_space(\r\n image: bpy.types.Image,\r\n color_space: str = 'sRGB'\r\n) -> None:\r\n \"\"\"\r\n Set image color space.\r\n \r\n Args:\r\n color_space: 'sRGB', 'Linear', 'Non-Color', 'Raw', etc.\r\n \"\"\"\r\n image.colorspace_settings.name = color_space\r\n\r\n\r\ndef resize_image(\r\n image: bpy.types.Image,\r\n width: int,\r\n height: int\r\n) -> None:\r\n \"\"\"Resize image.\"\"\"\r\n image.scale(width, height)\r\n\r\n\r\ndef flip_image(\r\n image: bpy.types.Image,\r\n horizontal: bool = False,\r\n vertical: bool = True\r\n) -> None:\r\n \"\"\"Flip image pixels.\"\"\"\r\n width = image.size[0]\r\n height = image.size[1]\r\n pixels = list(image.pixels)\r\n \r\n if vertical:\r\n for y in range(height // 2):\r\n for x in range(width):\r\n idx1 = (y * width + x) * 4\r\n idx2 = ((height - 1 - y) * width + x) * 4\r\n \r\n for c in range(4):\r\n pixels[idx1 + c], pixels[idx2 + c] = pixels[idx2 + c], pixels[idx1 + c]\r\n \r\n if horizontal:\r\n for y in range(height):\r\n for x in range(width // 2):\r\n idx1 = (y * width + x) * 4\r\n idx2 = (y * width + (width - 1 - x)) * 4\r\n \r\n for c in range(4):\r\n pixels[idx1 + c], pixels[idx2 + c] = pixels[idx2 + c], pixels[idx1 + c]\r\n \r\n image.pixels = pixels\r\n\r\n\r\ndef create_solid_color_image(\r\n name: str,\r\n color: tuple,\r\n size: int = 8\r\n) -> bpy.types.Image:\r\n \"\"\"Create small solid color image for materials.\"\"\"\r\n return create_image(name, size, size, color)\r\n\r\n\r\ndef create_checker_image(\r\n name: str,\r\n size: int = 512,\r\n squares: int = 8,\r\n color1: tuple = (0.2, 0.2, 0.2, 1),\r\n color2: tuple = (0.8, 0.8, 0.8, 1)\r\n) -> bpy.types.Image:\r\n \"\"\"Create checker pattern image.\"\"\"\r\n img = bpy.data.images.new(name, width=size, height=size)\r\n \r\n pixels = []\r\n square_size = size // squares\r\n \r\n for y in range(size):\r\n for x in range(size):\r\n checker = ((x // square_size) + (y // square_size)) % 2\r\n color = color1 if checker == 0 else color2\r\n pixels.extend(color)\r\n \r\n img.pixels = pixels\r\n return img\r\n\r\n\r\ndef create_gradient_image(\r\n name: str,\r\n width: int = 256,\r\n height: int = 256,\r\n direction: str = 'VERTICAL',\r\n color1: tuple = (0, 0, 0, 1),\r\n color2: tuple = (1, 1, 1, 1)\r\n) -> bpy.types.Image:\r\n \"\"\"\r\n Create gradient image.\r\n \r\n Args:\r\n direction: 'VERTICAL', 'HORIZONTAL', 'RADIAL'\r\n \"\"\"\r\n img = bpy.data.images.new(name, width=width, height=height)\r\n \r\n pixels = []\r\n \r\n for y in range(height):\r\n for x in range(width):\r\n if direction == 'VERTICAL':\r\n t = y / height\r\n elif direction == 'HORIZONTAL':\r\n t = x / width\r\n else: # RADIAL\r\n cx, cy = width/2, height/2\r\n dx, dy = x - cx, y - cy\r\n dist = (dx*dx + dy*dy) ** 0.5\r\n max_dist = min(cx, cy)\r\n t = min(1, dist / max_dist)\r\n \r\n # Lerp colors\r\n color = tuple(c1 * (1 - t) + c2 * t for c1, c2 in zip(color1, color2))\r\n pixels.extend(color)\r\n \r\n img.pixels = pixels\r\n return img\r\n\r\n\r\ndef get_image_resolution(image: bpy.types.Image) -> tuple:\r\n \"\"\"Get image dimensions.\"\"\"\r\n return (image.size[0], image.size[1])\r\n\r\n\r\ndef list_loaded_images() -> list:\r\n \"\"\"List all loaded image names.\"\"\"\r\n return [img.name for img in bpy.data.images]","category":"utility","source_file":"image_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Pack Image. Category: utility","output":"import bpy\n\ndef pack_image(image: bpy.types.Image) -> None:\r\n \"\"\"Pack image into blend file.\"\"\"\r\n if not image.packed_file:\r\n image.pack()\r\n\r\n\r\ndef unpack_image(\r\n image: bpy.types.Image,\r\n method: str = 'USE_ORIGINAL'\r\n) -> None:\r\n \"\"\"\r\n Unpack image to external file.\r\n \r\n Args:\r\n method: 'USE_ORIGINAL', 'WRITE_ORIGINAL', 'USE_LOCAL', 'WRITE_LOCAL'\r\n \"\"\"\r\n if image.packed_file:\r\n image.unpack(method=method)\r\n\r\n\r\ndef reload_image(image: bpy.types.Image) -> None:\r\n \"\"\"Reload image from disk.\"\"\"\r\n image.reload()\r\n\r\n\r\ndef get_image(name: str) -> bpy.types.Image:\r\n \"\"\"Get image by name.\"\"\"\r\n return bpy.data.images.get(name)\r\n\r\n\r\ndef set_color_space(\r\n image: bpy.types.Image,\r\n color_space: str = 'sRGB'\r\n) -> None:\r\n \"\"\"\r\n Set image color space.\r\n \r\n Args:\r\n color_space: 'sRGB', 'Linear', 'Non-Color', 'Raw', etc.\r\n \"\"\"\r\n image.colorspace_settings.name = color_space\r\n\r\n\r\ndef resize_image(\r\n image: bpy.types.Image,\r\n width: int,\r\n height: int\r\n) -> None:\r\n \"\"\"Resize image.\"\"\"\r\n image.scale(width, height)\r\n\r\n\r\ndef flip_image(\r\n image: bpy.types.Image,\r\n horizontal: bool = False,\r\n vertical: bool = True\r\n) -> None:\r\n \"\"\"Flip image pixels.\"\"\"\r\n width = image.size[0]\r\n height = image.size[1]\r\n pixels = list(image.pixels)\r\n \r\n if vertical:\r\n for y in range(height // 2):\n for x in range(width):\r\n idx1 = (y * width + x) * 4\r\n idx2 = ((height - 1 - y) * width + x) * 4\r\n \r\n for c in range(4):\r\n pixels[idx1 + c], pixels[idx2 + c] = pixels[idx2 + c], pixels[idx1 + c]\r\n \r\n if horizontal:\r\n for y in range(height):\r\n for x in range(width // 2):\r\n idx1 = (y * width + x) * 4\r\n idx2 = (y * width + (width - 1 - x)) * 4\r\n \r\n for c in range(4):\r\n pixels[idx1 + c], pixels[idx2 + c] = pixels[idx2 + c], pixels[idx1 + c]\r\n \r\n image.pixels = pixels","category":"utility","source_file":"image_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Get Image Resolution. Category: utility","output":"import bpy\n\ndef get_image_resolution(image: bpy.types.Image) -> tuple:\r\n \"\"\"Get image dimensions.\"\"\"\r\n return (image.size[0], image.size[1])\r\n\r\n\r\ndef list_loaded_images() -> list:\r\n \"\"\"List all loaded image names.\"\"\"\r\n return [img.name for img in bpy.data.images]","category":"utility","source_file":"image_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Interior Room & Architecture Recipes β€” Blender 4.0/5.0. Recipes for creating enclosed interior rooms, walls, doorways, and architectural structures. Essential for interior scene prompts (taverns, houses, dungeons, offices, etc.).","output":"import bpy\r\nimport bmesh\r\nimport math\r\n\r\n\r\ndef create_room(name=\"Room\", width=6, depth=8, height=3,\r\n wall_thickness=0.2, open_front=False):\r\n \"\"\"Create an enclosed room with floor, 4 walls, and ceiling.\r\n\r\n Creates separate objects for floor, walls, and ceiling so individual\r\n pieces can be hidden for camera placement.\r\n\r\n Args:\r\n name: Base name for room objects\r\n width: Room width (X axis) in meters\r\n depth: Room depth (Y axis) in meters\r\n height: Wall height in meters\r\n wall_thickness: Thickness of walls\r\n open_front: If True, omit front wall for camera access\r\n Returns:\r\n dict with 'floor', 'ceiling', 'walls' keys\r\n \"\"\"\r\n parts = {}\r\n\r\n # Floor\r\n bpy.ops.mesh.primitive_plane_add(size=1, location=(0, 0, 0))\r\n floor = bpy.context.active_object\r\n floor.name = f\"{name}_Floor\"\r\n floor.scale = (width / 2, depth / 2, 1)\r\n parts['floor'] = floor\r\n\r\n # Ceiling\r\n bpy.ops.mesh.primitive_plane_add(size=1, location=(0, 0, height))\r\n ceiling = bpy.context.active_object\r\n ceiling.name = f\"{name}_Ceiling\"\r\n ceiling.scale = (width / 2, depth / 2, 1)\r\n parts['ceiling'] = ceiling\r\n\r\n walls = []\r\n\r\n # Back wall (Y+)\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(0, depth / 2, height / 2))\r\n back = bpy.context.active_object\r\n back.name = f\"{name}_Wall_Back\"\r\n back.scale = (width / 2, wall_thickness / 2, height / 2)\r\n walls.append(back)\r\n\r\n # Left wall (X-)\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1, location=(-width / 2, 0, height / 2))\r\n left = bpy.context.active_object\r\n left.name = f\"{name}_Wall_Left\"\r\n left.scale = (wall_thickness / 2, depth / 2, height / 2)\r\n walls.append(left)\r\n\r\n # Right wall (X+)\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1, location=(width / 2, 0, height / 2))\r\n right = bpy.context.active_object\r\n right.name = f\"{name}_Wall_Right\"\r\n right.scale = (wall_thickness / 2, depth / 2, height / 2)\r\n walls.append(right)\r\n\r\n # Front wall (Y-) β€” optional\r\n if not open_front:\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1, location=(0, -depth / 2, height / 2))\r\n front = bpy.context.active_object\r\n front.name = f\"{name}_Wall_Front\"\r\n front.scale = (width / 2, wall_thickness / 2, height / 2)\r\n walls.append(front)\r\n\r\n parts['walls'] = walls\r\n return parts\r\n\r\n\r\ndef create_wall_with_doorway(name=\"DoorWall\", width=6, height=3,\r\n door_width=1.0, door_height=2.1,\r\n door_offset=0.0, thickness=0.2):\r\n \"\"\"Create a wall with a rectangular doorway cut out.\r\n\r\n Uses BMesh to create a wall plane with a hole for the door.\r\n The door is centered on door_offset from center of wall.\r\n\r\n Args:\r\n door_width: Width of doorway opening\r\n door_height: Height of doorway (standard: 2.1m)\r\n door_offset: Horizontal offset from wall center (0 = centered)\r\n thickness: Wall thickness\r\n \"\"\"\r\n mesh = bpy.data.meshes.new(f\"{name}_Mesh\")\r\n obj = bpy.data.objects.new(name, mesh)\r\n bpy.context.scene.collection.objects.link(obj)\r\n\r\n bm = bmesh.new()\r\n\r\n half_w = width / 2\r\n half_d = door_width / 2\r\n\r\n # Create wall as 4 faces around the doorway opening\r\n # Bottom-left section (below door isn't needed, door starts at floor)\r\n # Left of door\r\n if door_offset - half_d > -half_w:\r\n verts_left = [\r\n bm.verts.new((-half_w, 0, 0)),\r\n bm.verts.new((door_offset - half_d, 0, 0)),\r\n bm.verts.new((door_offset - half_d, 0, height)),\r\n bm.verts.new((-half_w, 0, height)),\r\n ]\r\n bm.faces.new(verts_left)\r\n\r\n # Right of door\r\n if door_offset + half_d < half_w:\r\n verts_right = [\r\n bm.verts.new((door_offset + half_d, 0, 0)),\r\n bm.verts.new((half_w, 0, 0)),\r\n bm.verts.new((half_w, 0, height)),\r\n bm.verts.new((door_offset + half_d, 0, height)),\r\n ]\r\n bm.faces.new(verts_right)\r\n\r\n # Above door\r\n verts_top = [\r\n bm.verts.new((door_offset - half_d, 0, door_height)),\r\n bm.verts.new((door_offset + half_d, 0, door_height)),\r\n bm.verts.new((door_offset + half_d, 0, height)),\r\n bm.verts.new((door_offset - half_d, 0, height)),\r\n ]\r\n bm.faces.new(verts_top)\r\n\r\n bm.to_mesh(mesh)\r\n bm.free()\r\n mesh.update(calc_edges=True)\r\n\r\n # Add Solidify modifier for wall thickness\r\n solidify = obj.modifiers.new(name=\"Solidify\", type='SOLIDIFY')\r\n solidify.thickness = thickness\r\n solidify.offset = 0 # Center thickness on face\r\n\r\n return obj\r\n\r\n\r\ndef create_wall_with_window(name=\"WindowWall\", width=6, height=3,\r\n win_width=1.2, win_height=1.0,\r\n win_bottom=1.0, win_offset=0.0,\r\n thickness=0.2):\r\n \"\"\"Create a wall with a rectangular window opening.\r\n\r\n Args:\r\n win_width: Window width\r\n win_height: Window height\r\n win_bottom: Height from floor to bottom of window (sill height)\r\n win_offset: Horizontal offset from center\r\n thickness: Wall thickness\r\n \"\"\"\r\n mesh = bpy.data.meshes.new(f\"{name}_Mesh\")\r\n obj = bpy.data.objects.new(name, mesh)\r\n bpy.context.scene.collection.objects.link(obj)\r\n\r\n bm = bmesh.new()\r\n\r\n half_w = width / 2\r\n half_win = win_width / 2\r\n win_top = win_bottom + win_height\r\n wo = win_offset # shorthand\r\n\r\n # Below window (full width)\r\n v_below = [\r\n bm.verts.new((-half_w, 0, 0)),\r\n bm.verts.new((half_w, 0, 0)),\r\n bm.verts.new((half_w, 0, win_bottom)),\r\n bm.verts.new((-half_w, 0, win_bottom)),\r\n ]\r\n bm.faces.new(v_below)\r\n\r\n # Above window (full width)\r\n v_above = [\r\n bm.verts.new((-half_w, 0, win_top)),\r\n bm.verts.new((half_w, 0, win_top)),\r\n bm.verts.new((half_w, 0, height)),\r\n bm.verts.new((-half_w, 0, height)),\r\n ]\r\n bm.faces.new(v_above)\r\n\r\n # Left of window\r\n if wo - half_win > -half_w:\r\n v_left = [\r\n bm.verts.new((-half_w, 0, win_bottom)),\r\n bm.verts.new((wo - half_win, 0, win_bottom)),\r\n bm.verts.new((wo - half_win, 0, win_top)),\r\n bm.verts.new((-half_w, 0, win_top)),\r\n ]\r\n bm.faces.new(v_left)\r\n\r\n # Right of window\r\n if wo + half_win < half_w:\r\n v_right = [\r\n bm.verts.new((wo + half_win, 0, win_bottom)),\r\n bm.verts.new((half_w, 0, win_bottom)),\r\n bm.verts.new((half_w, 0, win_top)),\r\n bm.verts.new((wo + half_win, 0, win_top)),\r\n ]\r\n bm.faces.new(v_right)\r\n\r\n bm.to_mesh(mesh)\r\n bm.free()\r\n mesh.update(calc_edges=True)\r\n\r\n solidify = obj.modifiers.new(name=\"Solidify\", type='SOLIDIFY')\r\n solidify.thickness = thickness\r\n\r\n return obj\r\n\r\n\r\ndef create_stone_wall_material(name=\"StoneMaterial\"):\r\n \"\"\"Quick stone/masonry material for walls.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.45, 0.42, 0.38, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n return mat\r\n\r\n\r\ndef create_wood_material(name=\"WoodMaterial\"):\r\n \"\"\"Quick warm wood material for floors and furniture.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.22, 0.12, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n return mat","category":"utility","source_file":"interior_rooms.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Quick stone/masonry material for walls.","output":"import bpy\n\ndef create_stone_wall_material(name=\"StoneMaterial\"):\n \"\"\"Quick stone/masonry material for walls.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.45, 0.42, 0.38, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n return mat","category":"utility","source_file":"interior_rooms.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Quick warm wood material for floors and furniture.","output":"import bpy\n\ndef create_wood_material(name=\"WoodMaterial\"):\n \"\"\"Quick warm wood material for floors and furniture.\"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.22, 0.12, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n return mat","category":"utility","source_file":"interior_rooms.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Import/Export Utilities. Functions for importing and exporting 3D file formats.","output":"import bpy\r\nimport os\r\n\r\n\r\ndef export_gltf(\r\n filepath: str,\r\n export_selected: bool = True,\r\n export_animations: bool = True,\r\n export_materials: bool = True\r\n) -> None:\r\n \"\"\"\r\n Export to glTF/GLB format.\r\n \r\n Args:\r\n filepath: Output path (.gltf or .glb)\r\n export_selected: Only export selected objects\r\n export_animations: Include animations\r\n export_materials: Include materials\r\n \"\"\"\r\n bpy.ops.export_scene.gltf(\r\n filepath=filepath,\r\n use_selection=export_selected,\r\n export_animations=export_animations,\r\n export_materials='EXPORT' if export_materials else 'NONE'\r\n )\r\n\r\n\r\ndef import_gltf(filepath: str) -> list:\r\n \"\"\"Import glTF/GLB file. Returns imported objects.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.import_scene.gltf(filepath=filepath)\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef export_fbx(\r\n filepath: str,\r\n export_selected: bool = True,\r\n apply_modifiers: bool = True,\r\n include_armature: bool = True\r\n) -> None:\r\n \"\"\"Export to FBX format.\"\"\"\r\n bpy.ops.export_scene.fbx(\r\n filepath=filepath,\r\n use_selection=export_selected,\r\n apply_modifiers=apply_modifiers,\r\n add_leaf_bones=False,\r\n bake_anim=include_armature\r\n )\r\n\r\n\r\ndef import_fbx(filepath: str, auto_bone_orientation: bool = True) -> list:\r\n \"\"\"Import FBX file.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.import_scene.fbx(\r\n filepath=filepath,\r\n automatic_bone_orientation=auto_bone_orientation\r\n )\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef export_obj(\r\n filepath: str,\r\n export_selected: bool = True,\r\n export_materials: bool = True\r\n) -> None:\r\n \"\"\"Export to OBJ format.\"\"\"\r\n bpy.ops.wm.obj_export(\r\n filepath=filepath,\r\n export_selected_objects=export_selected,\r\n export_materials=export_materials\r\n )\r\n\r\n\r\ndef import_obj(filepath: str) -> list:\r\n \"\"\"Import OBJ file.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.wm.obj_import(filepath=filepath)\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef export_stl(\r\n filepath: str,\r\n export_selected: bool = True,\r\n ascii_format: bool = False\r\n) -> None:\r\n \"\"\"Export to STL format for 3D printing.\"\"\"\r\n bpy.ops.export_mesh.stl(\r\n filepath=filepath,\r\n use_selection=export_selected,\r\n ascii=ascii_format\r\n )\r\n\r\n\r\ndef import_stl(filepath: str) -> list:\r\n \"\"\"Import STL file.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.import_mesh.stl(filepath=filepath)\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef export_usd(\r\n filepath: str,\r\n export_selected: bool = True\r\n) -> None:\r\n \"\"\"Export to USD/USDA/USDC format.\"\"\"\r\n bpy.ops.wm.usd_export(\r\n filepath=filepath,\r\n selected_objects_only=export_selected\r\n )\r\n\r\n\r\ndef import_usd(filepath: str) -> list:\r\n \"\"\"Import USD file.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.wm.usd_import(filepath=filepath)\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef batch_export(\r\n directory: str,\r\n format: str = 'GLTF',\r\n separate_files: bool = True\r\n) -> list:\r\n \"\"\"\r\n Export all/selected objects to directory.\r\n \r\n Args:\r\n directory: Output directory\r\n format: 'GLTF', 'FBX', 'OBJ', 'STL'\r\n separate_files: One file per object\r\n \r\n Returns:\r\n List of exported file paths\r\n \"\"\"\r\n os.makedirs(directory, exist_ok=True)\r\n exported = []\r\n \r\n objects = bpy.context.selected_objects or bpy.context.scene.objects\r\n \r\n if separate_files:\r\n for obj in objects:\r\n if obj.type == 'MESH':\r\n bpy.ops.object.select_all(action='DESELECT')\r\n obj.select_set(True)\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n ext = {'GLTF': '.glb', 'FBX': '.fbx', 'OBJ': '.obj', 'STL': '.stl'}\r\n filepath = os.path.join(directory, obj.name + ext.get(format, '.glb'))\r\n \r\n if format == 'GLTF':\r\n export_gltf(filepath, export_selected=True)\r\n elif format == 'FBX':\r\n export_fbx(filepath, export_selected=True)\r\n elif format == 'OBJ':\r\n export_obj(filepath, export_selected=True)\r\n elif format == 'STL':\r\n export_stl(filepath, export_selected=True)\r\n \r\n exported.append(filepath)\r\n \r\n return exported","category":"utility","source_file":"io_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Import Gltf. Category: utility","output":"import bpy\n\ndef import_gltf(filepath: str) -> list:\r\n \"\"\"Import glTF/GLB file. Returns imported objects.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.import_scene.gltf(filepath=filepath)\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef export_fbx(\r\n filepath: str,\r\n export_selected: bool = True,\r\n apply_modifiers: bool = True,\r\n include_armature: bool = True\r\n) -> None:\r\n \"\"\"Export to FBX format.\"\"\"\r\n bpy.ops.export_scene.fbx(\r\n filepath=filepath,\r\n use_selection=export_selected,\r\n apply_modifiers=apply_modifiers,\r\n add_leaf_bones=False,\r\n bake_anim=include_armature\r\n )\r\n\r\n\r\ndef import_fbx(filepath: str, auto_bone_orientation: bool = True) -> list:\r\n \"\"\"Import FBX file.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.import_scene.fbx(\r\n filepath=filepath,\r\n automatic_bone_orientation=auto_bone_orientation\r\n )\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef export_obj(\r\n filepath: str,\r\n export_selected: bool = True,\r\n export_materials: bool = True\r\n) -> None:\r\n \"\"\"Export to OBJ format.\"\"\"\r\n bpy.ops.wm.obj_export(\r\n filepath=filepath,\r\n export_selected_objects=export_selected,\r\n export_materials=export_materials\r\n )\r\n\r\n\r\ndef import_obj(filepath: str) -> list:\r\n \"\"\"Import OBJ file.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.wm.obj_import(filepath=filepath)\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef export_stl(\r\n filepath: str,\r\n export_selected: bool = True,\r\n ascii_format: bool = False\r\n) -> None:\r\n \"\"\"Export to STL format for 3D printing.\"\"\"\r\n bpy.ops.export_mesh.stl(\r\n filepath=filepath,\r\n use_selection=export_selected,\r\n ascii=ascii_format\r\n )\r\n\r\n\r\ndef import_stl(filepath: str) -> list:\r\n \"\"\"Import STL file.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.import_mesh.stl(filepath=filepath)\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef export_usd(\r\n filepath: str,\r\n export_selected: bool = True\r\n) -> None:\r\n \"\"\"Export to USD/USDA/USDC format.\"\"\"\r\n bpy.ops.wm.usd_export(\r\n filepath=filepath,\r\n selected_objects_only=export_selected\r\n )\r\n\r\n\r\ndef import_usd(filepath: str) -> list:\r\n \"\"\"Import USD file.\"\"\"\r\n before = set(bpy.data.objects)\r\n bpy.ops.wm.usd_import(filepath=filepath)\r\n after = set(bpy.data.objects)\r\n return list(after - before)\r\n\r\n\r\ndef batch_export(\r\n directory: str,\r\n format: str = 'GLTF',\r\n separate_files: bool = True\r\n) -> list:\r\n \"\"\"\r\n Export all/selected objects to directory.\r\n \r\n Args:\r\n directory: Output directory\r\n format: 'GLTF', 'FBX', 'OBJ', 'STL'\r\n separate_files: One file per object\r\n \r\n Returns:\r\n List of exported file paths\r\n \"\"\"\r\n os.makedirs(directory, exist_ok=True)\r\n exported = []\r\n \r\n objects = bpy.context.selected_objects or bpy.context.scene.objects\r\n \r\n if separate_files:\r\n for obj in objects:\r\n if obj.type == 'MESH':\r\n bpy.ops.object.select_all(action='DESELECT')\r\n obj.select_set(True)\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n ext = {'GLTF': '.glb', 'FBX': '.fbx', 'OBJ': '.obj', 'STL': '.stl'}\r\n filepath = os.path.join(directory, obj.name + ext.get(format, '.glb'))\r\n \r\n if format == 'GLTF':\r\n export_gltf(filepath, export_selected=True)\r\n elif format == 'FBX':\r\n export_fbx(filepath, export_selected=True)\r\n elif format == 'OBJ':\r\n export_obj(filepath, export_selected=True)\r\n elif format == 'STL':\r\n export_stl(filepath, export_selected=True)\r\n \r\n exported.append(filepath)\r\n \r\n return exported","category":"utility","source_file":"io_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Professional Lighting Recipes for Blender. Professional lighting setups used in film, product visualization, and architectural rendering. Includes three-point lighting, studio setups, outdoor/sunset lighting, dramatic rim lighting, and HDRI environment lighting. Each recipe creates complete light rigs with correct energy, color temperature, and shadow settings.","output":"import bpy\r\nimport math\r\n\r\n\r\n# =============================================================================\r\n# PROFESSIONAL LIGHTING RECIPES\r\n# =============================================================================\r\n#\r\n# IMPORTANT PRINCIPLES:\r\n# 1. Key light = main light source, strongest, casts primary shadows\r\n# 2. Fill light = softer, opposite side from key, reduces harsh shadows\r\n# 3. Rim/back light = behind subject, creates edge highlight/separation\r\n# 4. Use color temperature to set mood: warm (orange/yellow) or cool (blue)\r\n# 5. Energy values below are calibrated for Material Preview mode\r\n# 6. For Cycles renders, multiply energy by ~10\r\n#\r\n# COLOR TEMPERATURE REFERENCE (RGB approximations):\r\n# Candle (1800K): (1.0, 0.58, 0.16)\r\n# Warm tungsten (2700K): (1.0, 0.76, 0.46)\r\n# Warm white (3500K): (1.0, 0.84, 0.66)\r\n# Neutral (4500K): (1.0, 0.92, 0.82)\r\n# Daylight (5500K): (1.0, 0.96, 0.92)\r\n# Overcast (6500K): (0.87, 0.91, 1.0)\r\n# Blue sky (8000K): (0.75, 0.83, 1.0)\r\n# =============================================================================\r\n\r\n\r\ndef setup_three_point_lighting(\r\n target_location=(0, 0, 0),\r\n key_energy=800,\r\n fill_energy=300,\r\n rim_energy=500,\r\n key_color=(1.0, 0.95, 0.88),\r\n fill_color=(0.75, 0.83, 1.0),\r\n rim_color=(1.0, 1.0, 1.0),\r\n prefix=\"Light\"\r\n):\r\n \"\"\"\r\n Classic three-point lighting setup used in film and photography.\r\n \r\n Creates:\r\n - Key light: Main light, 45Β° above and to the right\r\n - Fill light: Softer, opposite side, reduces shadows\r\n - Rim light: Behind subject, creates edge separation\r\n \r\n Args:\r\n target_location: Center point the lights aim at\r\n key_energy: Key light power (default 800 for EEVEE)\r\n fill_energy: Fill light power (typically 30-50% of key)\r\n rim_energy: Rim light power\r\n key_color: Key light color (warm daylight default)\r\n fill_color: Fill light color (cool blue default for contrast)\r\n rim_color: Rim light color\r\n prefix: Name prefix for light objects\r\n \"\"\"\r\n tx, ty, tz = target_location\r\n \r\n # Key Light β€” 45Β° right, 45Β° above\r\n bpy.ops.object.light_add(type='AREA', location=(tx + 5, ty - 5, tz + 5))\r\n key = bpy.context.active_object\r\n key.name = f\"{prefix}_Key\"\r\n key.data.energy = key_energy\r\n key.data.color = key_color\r\n key.data.size = 2.0 # Soft shadows\r\n key.data.shadow_soft_size = 2.0\r\n # Point at target\r\n direction = (tx - key.location.x, ty - key.location.y, tz - key.location.z)\r\n key.rotation_euler = _look_at_rotation(key.location, target_location)\r\n \r\n # Fill Light β€” opposite side, lower, softer\r\n bpy.ops.object.light_add(type='AREA', location=(tx - 4, ty - 3, tz + 2))\r\n fill = bpy.context.active_object\r\n fill.name = f\"{prefix}_Fill\"\r\n fill.data.energy = fill_energy\r\n fill.data.color = fill_color\r\n fill.data.size = 4.0 # Very soft (larger = softer shadows)\r\n fill.rotation_euler = _look_at_rotation(fill.location, target_location)\r\n \r\n # Rim/Back Light β€” behind subject, high\r\n bpy.ops.object.light_add(type='SPOT', location=(tx + 1, ty + 6, tz + 4))\r\n rim = bpy.context.active_object\r\n rim.name = f\"{prefix}_Rim\"\r\n rim.data.energy = rim_energy\r\n rim.data.color = rim_color\r\n rim.data.spot_size = math.radians(60)\r\n rim.data.spot_blend = 0.5\r\n rim.rotation_euler = _look_at_rotation(rim.location, target_location)\r\n \r\n return key, fill, rim\r\n\r\n\r\ndef setup_studio_lighting(\r\n target_location=(0, 0, 0),\r\n energy_multiplier=1.0,\r\n warm=True,\r\n prefix=\"Studio\"\r\n):\r\n \"\"\"\r\n Professional studio lighting for product visualization.\r\n \r\n Creates a 4-light setup:\r\n - Large key area light (top-right)\r\n - Large fill area light (top-left)\r\n - Ground bounce light (below, subtle)\r\n - Background light (behind, for backdrop illumination)\r\n \r\n Ideal for: product shots, character portraits, turntable renders\r\n \"\"\"\r\n tx, ty, tz = target_location\r\n base_color = (1.0, 0.95, 0.88) if warm else (0.92, 0.95, 1.0)\r\n \r\n # Key β€” large overhead area\r\n bpy.ops.object.light_add(type='AREA', location=(tx + 4, ty - 3, tz + 6))\r\n key = bpy.context.active_object\r\n key.name = f\"{prefix}_Key\"\r\n key.data.energy = 600 * energy_multiplier\r\n key.data.color = base_color\r\n key.data.size = 5.0\r\n key.data.shape = 'RECTANGLE'\r\n key.data.size_y = 3.0\r\n key.rotation_euler = _look_at_rotation(key.location, target_location)\r\n \r\n # Fill β€” opposite side, very soft\r\n bpy.ops.object.light_add(type='AREA', location=(tx - 5, ty - 2, tz + 4))\r\n fill = bpy.context.active_object\r\n fill.name = f\"{prefix}_Fill\"\r\n fill.data.energy = 250 * energy_multiplier\r\n fill.data.color = base_color\r\n fill.data.size = 6.0\r\n fill.rotation_euler = _look_at_rotation(fill.location, target_location)\r\n \r\n # Ground bounce β€” subtle uplight\r\n bpy.ops.object.light_add(type='AREA', location=(tx, ty, tz - 1))\r\n bounce = bpy.context.active_object\r\n bounce.name = f\"{prefix}_Bounce\"\r\n bounce.data.energy = 100 * energy_multiplier\r\n bounce.data.color = (1.0, 1.0, 1.0)\r\n bounce.data.size = 8.0\r\n bounce.rotation_euler = (math.radians(180), 0, 0)\r\n \r\n # Background\r\n bpy.ops.object.light_add(type='AREA', location=(tx, ty + 5, tz + 2))\r\n bg = bpy.context.active_object\r\n bg.name = f\"{prefix}_BG\"\r\n bg.data.energy = 200 * energy_multiplier\r\n bg.data.color = base_color\r\n bg.data.size = 8.0\r\n bg.rotation_euler = _look_at_rotation(bg.location, (tx, ty, tz))\r\n \r\n return key, fill, bounce, bg\r\n\r\n\r\ndef setup_outdoor_sunlight(\r\n sun_direction=(0.5, -0.3, -0.8),\r\n sun_energy=3.0,\r\n sun_color=(1.0, 0.95, 0.85),\r\n sky_color=(0.3, 0.55, 0.9),\r\n prefix=\"Outdoor\"\r\n):\r\n \"\"\"\r\n Outdoor daylight setup with sun and sky fill.\r\n \r\n Creates:\r\n - Sun light (directional, parallel rays)\r\n - Sky fill light (large area light from above, blue tint)\r\n \r\n Args:\r\n sun_direction: Direction vector for sunlight (negative Z = downward)\r\n sun_energy: Sun intensity (3–5 typical for EEVEE)\r\n sun_color: Sun color (warm for golden hour, neutral for midday)\r\n sky_color: Ambient sky color\r\n \"\"\"\r\n # Sun light\r\n bpy.ops.object.light_add(type='SUN', location=(0, 0, 10))\r\n sun = bpy.context.active_object\r\n sun.name = f\"{prefix}_Sun\"\r\n sun.data.energy = sun_energy\r\n sun.data.color = sun_color\r\n sun.data.angle = math.radians(0.5) # Small angle = sharp shadows\r\n \r\n # Aim the sun\r\n dx, dy, dz = sun_direction\r\n sun.rotation_euler = (\r\n math.atan2(math.sqrt(dx*dx + dy*dy), -dz),\r\n 0,\r\n math.atan2(dx, -dy)\r\n )\r\n \r\n # Sky fill β€” large blue area light from above\r\n bpy.ops.object.light_add(type='AREA', location=(0, 0, 15))\r\n sky = bpy.context.active_object\r\n sky.name = f\"{prefix}_Sky\"\r\n sky.data.energy = 100\r\n sky.data.color = sky_color\r\n sky.data.size = 20.0\r\n sky.rotation_euler = (math.radians(180), 0, 0) # Point down\r\n \r\n return sun, sky\r\n\r\n\r\ndef setup_sunset_lighting(\r\n target_location=(0, 0, 0),\r\n prefix=\"Sunset\"\r\n):\r\n \"\"\"\r\n Golden hour / sunset lighting with warm orange key and cool blue fill.\r\n \r\n Creates dramatic warm-cool contrast typical of sunset scenes.\r\n \"\"\"\r\n tx, ty, tz = target_location\r\n \r\n # Low sun β€” warm orange, from the side\r\n bpy.ops.object.light_add(type='SUN', location=(10, -5, 2))\r\n sun = bpy.context.active_object\r\n sun.name = f\"{prefix}_Sun\"\r\n sun.data.energy = 4.0\r\n sun.data.color = (1.0, 0.6, 0.2) # Warm orange\r\n sun.data.angle = math.radians(2.0) # Slightly soft\r\n sun.rotation_euler = _look_at_rotation(sun.location, target_location)\r\n \r\n # Cool sky fill from above\r\n bpy.ops.object.light_add(type='AREA', location=(tx, ty, tz + 12))\r\n sky = bpy.context.active_object\r\n sky.name = f\"{prefix}_Sky\"\r\n sky.data.energy = 80\r\n sky.data.color = (0.5, 0.6, 1.0) # Cool blue\r\n sky.data.size = 15.0\r\n sky.rotation_euler = (math.radians(180), 0, 0)\r\n \r\n return sun, sky\r\n\r\n\r\ndef setup_dramatic_lighting(\r\n target_location=(0, 0, 0),\r\n prefix=\"Dramatic\"\r\n):\r\n \"\"\"\r\n High-contrast dramatic lighting for moody/cinematic scenes.\r\n \r\n Features: strong key from one side, almost no fill,\r\n strong colored rim light for edge separation.\r\n \"\"\"\r\n tx, ty, tz = target_location\r\n \r\n # Key β€” harsh, single side\r\n bpy.ops.object.light_add(type='SPOT', location=(tx + 5, ty - 2, tz + 4))\r\n key = bpy.context.active_object\r\n key.name = f\"{prefix}_Key\"\r\n key.data.energy = 1200\r\n key.data.color = (1.0, 0.9, 0.75)\r\n key.data.spot_size = math.radians(45)\r\n key.data.spot_blend = 0.3\r\n key.rotation_euler = _look_at_rotation(key.location, target_location)\r\n \r\n # Rim β€” colored, strong\r\n bpy.ops.object.light_add(type='SPOT', location=(tx - 2, ty + 5, tz + 3))\r\n rim = bpy.context.active_object\r\n rim.name = f\"{prefix}_Rim\"\r\n rim.data.energy = 800\r\n rim.data.color = (0.3, 0.5, 1.0) # Blue rim\r\n rim.data.spot_size = math.radians(50)\r\n rim.data.spot_blend = 0.4\r\n rim.rotation_euler = _look_at_rotation(rim.location, target_location)\r\n \r\n return key, rim\r\n\r\n\r\ndef set_world_hdri(hdri_path: str, strength: float = 1.0):\r\n \"\"\"\r\n Set up an HDRI environment map for realistic lighting.\r\n \r\n Args:\r\n hdri_path: File path to .hdr or .exr file\r\n strength: Light intensity multiplier\r\n \"\"\"\r\n world = bpy.data.worlds.get(\"World\") or bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n # Background node\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.inputs['Strength'].default_value = strength\r\n \r\n # Environment texture\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n # Optional: Texture coordinate for rotation\r\n coord = nodes.new('ShaderNodeTexCoord')\r\n mapping = nodes.new('ShaderNodeMapping')\r\n \r\n links.new(coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n \r\n return world\r\n\r\n\r\ndef set_world_color(color=(0.05, 0.05, 0.1), strength=1.0):\r\n \"\"\"\r\n Set a solid-color world background.\r\n \r\n Common backgrounds:\r\n Dark studio: (0.02, 0.02, 0.02)\r\n Space/night: (0.002, 0.002, 0.01)\r\n Light studio: (0.8, 0.8, 0.82)\r\n Overcast sky: (0.5, 0.55, 0.6)\r\n Navy blue: (0.02, 0.03, 0.08)\r\n \"\"\"\r\n world = bpy.data.worlds.get(\"World\") or bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.inputs['Color'].default_value = (*color, 1.0)\r\n bg.inputs['Strength'].default_value = strength\r\n \r\n output = nodes.new('ShaderNodeOutputWorld')\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n \r\n return world\r\n\r\n\r\n# =============================================================================\r\n# HELPER\r\n# =============================================================================\r\n\r\ndef _look_at_rotation(source_location, target_location):\r\n \"\"\"Calculate rotation euler for an object to look at a target point.\"\"\"\r\n from mathutils import Vector, Matrix\r\n direction = Vector(target_location) - Vector(source_location)\r\n rot_quat = direction.to_track_quat('-Z', 'Y')\r\n return rot_quat.to_euler()","category":"utility","source_file":"lighting_recipes.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Set up an HDRI environment map for realistic lighting.\r\n \r\n Args:\r\n hdri_path: File path to .hdr or .exr file\r\n strength: Light intensity multiplier","output":"import bpy\n\ndef set_world_hdri(hdri_path: str, strength: float = 1.0):\n \"\"\"\r\n Set up an HDRI environment map for realistic lighting.\r\n \r\n Args:\r\n hdri_path: File path to .hdr or .exr file\r\n strength: Light intensity multiplier\r\n \"\"\"\r\n world = bpy.data.worlds.get(\"World\") or bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n # Background node\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.inputs['Strength'].default_value = strength\r\n \r\n # Environment texture\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n # Optional: Texture coordinate for rotation\r\n coord = nodes.new('ShaderNodeTexCoord')\r\n mapping = nodes.new('ShaderNodeMapping')\r\n \r\n links.new(coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n \r\n return world","category":"utility","source_file":"lighting_recipes.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Set a solid-color world background.\r\n \r\n Common backgrounds:\r\n Dark studio: (0.02, 0.02, 0.02)\r\n Space/night: (0.002, 0.002, 0.01)\r\n Light studio: (0.8, 0.8, 0.82)\r\n Overcast sky: (0.5, 0.55, 0.6)\r\n Navy blue: (0.02, 0.03, 0.08)","output":"import bpy\n\ndef set_world_color(color=(0.05, 0.05, 0.1), strength=1.0):\n \"\"\"\r\n Set a solid-color world background.\r\n \r\n Common backgrounds:\r\n Dark studio: (0.02, 0.02, 0.02)\r\n Space/night: (0.002, 0.002, 0.01)\r\n Light studio: (0.8, 0.8, 0.82)\r\n Overcast sky: (0.5, 0.55, 0.6)\r\n Navy blue: (0.02, 0.03, 0.08)\r\n \"\"\"\r\n world = bpy.data.worlds.get(\"World\") or bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.inputs['Color'].default_value = (*color, 1.0)\r\n bg.inputs['Strength'].default_value = strength\r\n \r\n output = nodes.new('ShaderNodeOutputWorld')\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n \r\n return world","category":"utility","source_file":"lighting_recipes.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Lighting Utilities. Functions for creating and configuring different types of lights in Blender scenes.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef add_point_light(\r\n location: tuple = (0, 0, 3),\r\n energy: float = 1000,\r\n color: tuple = (1.0, 1.0, 1.0),\r\n radius: float = 0.25,\r\n name: str = \"PointLight\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add a point light to the scene.\r\n \r\n Args:\r\n location: XYZ position tuple\r\n energy: Light intensity in watts\r\n color: RGB color tuple (0.0-1.0)\r\n radius: Shadow softness radius\r\n name: Object name\r\n \r\n Returns:\r\n The created light object\r\n \r\n Example:\r\n >>> warm_light = add_point_light((2, -2, 4), 800, (1.0, 0.9, 0.8))\r\n \"\"\"\r\n bpy.ops.object.light_add(type='POINT', location=location)\r\n light = bpy.context.active_object\r\n light.name = name\r\n light.data.energy = energy\r\n light.data.color = color\r\n light.data.shadow_soft_size = radius\r\n return light\r\n\r\n\r\ndef add_area_light(\r\n location: tuple = (0, 0, 3),\r\n rotation: tuple = (0, 0, 0),\r\n energy: float = 500,\r\n color: tuple = (1.0, 1.0, 1.0),\r\n size: float = 2.0,\r\n shape: str = 'RECTANGLE',\r\n size_y: float = None,\r\n name: str = \"AreaLight\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add an area light - ideal for soft shadows and studio lighting.\r\n \r\n Args:\r\n location: XYZ position tuple\r\n rotation: XYZ rotation in degrees\r\n energy: Light intensity in watts\r\n color: RGB color tuple\r\n size: Width of the light\r\n shape: 'SQUARE', 'RECTANGLE', 'DISK', or 'ELLIPSE'\r\n size_y: Height (for RECTANGLE/ELLIPSE), defaults to size\r\n name: Object name\r\n \r\n Example:\r\n >>> key_light = add_area_light((5, -5, 6), (45, 0, 45), 1200, size=4)\r\n \"\"\"\r\n bpy.ops.object.light_add(type='AREA', location=location)\r\n light = bpy.context.active_object\r\n light.name = name\r\n light.rotation_euler = tuple(math.radians(r) for r in rotation)\r\n light.data.energy = energy\r\n light.data.color = color\r\n light.data.shape = shape\r\n light.data.size = size\r\n if size_y is not None:\r\n light.data.size_y = size_y\r\n elif shape in ('RECTANGLE', 'ELLIPSE'):\r\n light.data.size_y = size * 0.6\r\n return light\r\n\r\n\r\ndef add_sun_light(\r\n rotation: tuple = (45, 0, 30),\r\n energy: float = 5,\r\n color: tuple = (1.0, 0.98, 0.95),\r\n angle: float = 0.01,\r\n name: str = \"Sun\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add a sun light for outdoor scenes with parallel rays.\r\n \r\n Args:\r\n rotation: XYZ rotation in degrees (controls sun direction)\r\n energy: Light intensity\r\n color: RGB color tuple\r\n angle: Angular diameter for soft shadows (radians)\r\n name: Object name\r\n \r\n Example:\r\n >>> sun = add_sun_light((60, 0, -30), energy=8)\r\n \"\"\"\r\n bpy.ops.object.light_add(type='SUN', location=(0, 0, 10))\r\n light = bpy.context.active_object\r\n light.name = name\r\n light.rotation_euler = tuple(math.radians(r) for r in rotation)\r\n light.data.energy = energy\r\n light.data.color = color\r\n light.data.angle = angle\r\n return light\r\n\r\n\r\ndef add_spot_light(\r\n location: tuple = (0, 0, 5),\r\n rotation: tuple = (0, 0, 0),\r\n energy: float = 1000,\r\n color: tuple = (1.0, 1.0, 1.0),\r\n spot_size: float = 45,\r\n spot_blend: float = 0.15,\r\n radius: float = 0.1,\r\n name: str = \"SpotLight\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add a spot light with cone-shaped illumination.\r\n \r\n Args:\r\n location: XYZ position tuple\r\n rotation: XYZ rotation in degrees\r\n energy: Light intensity in watts\r\n color: RGB color tuple\r\n spot_size: Cone angle in degrees\r\n spot_blend: Edge softness (0-1)\r\n radius: Shadow softness\r\n name: Object name\r\n \r\n Example:\r\n >>> stage_spot = add_spot_light((0, -5, 8), (60, 0, 0), 2000, spot_size=30)\r\n \"\"\"\r\n bpy.ops.object.light_add(type='SPOT', location=location)\r\n light = bpy.context.active_object\r\n light.name = name\r\n light.rotation_euler = tuple(math.radians(r) for r in rotation)\r\n light.data.energy = energy\r\n light.data.color = color\r\n light.data.spot_size = math.radians(spot_size)\r\n light.data.spot_blend = spot_blend\r\n light.data.shadow_soft_size = radius\r\n return light\r\n\r\n\r\ndef set_world_hdri(hdri_path: str, strength: float = 1.0, rotation_z: float = 0) -> None:\r\n \"\"\"\r\n Set up world environment with an HDRI image for realistic lighting.\r\n \r\n Args:\r\n hdri_path: File path to .hdr or .exr file\r\n strength: Environment light intensity\r\n rotation_z: Rotate environment horizontally (degrees)\r\n \r\n Example:\r\n >>> set_world_hdri(\"/path/to/studio.hdr\", strength=1.5, rotation_z=90)\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n # Create nodes\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n mapping = nodes.new('ShaderNodeMapping')\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n background = nodes.new('ShaderNodeBackground')\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n # Set values\r\n mapping.inputs['Rotation'].default_value[2] = math.radians(rotation_z)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n # Link nodes\r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef set_world_color(color: tuple = (0.05, 0.05, 0.05), strength: float = 1.0) -> None:\r\n \"\"\"\r\n Set world background to a solid color.\r\n \r\n Args:\r\n color: RGB color tuple\r\n strength: Background intensity\r\n \r\n Example:\r\n >>> set_world_color((0.02, 0.02, 0.05)) # Dark blue background\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n background = nodes.new('ShaderNodeBackground')\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n background.inputs['Color'].default_value = (*color, 1.0)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n links.new(background.outputs['Background'], output.inputs['Surface'])","category":"utility","source_file":"lighting_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Set World Hdri. Category: utility","output":"import bpy\n\ndef set_world_hdri(hdri_path: str, strength: float = 1.0, rotation_z: float = 0) -> None:\r\n \"\"\"\r\n Set up world environment with an HDRI image for realistic lighting.\r\n \r\n Args:\r\n hdri_path: File path to .hdr or .exr file\r\n strength: Environment light intensity\r\n rotation_z: Rotate environment horizontally (degrees)\r\n \r\n Example:\r\n >>> set_world_hdri(\"/path/to/studio.hdr\", strength=1.5, rotation_z=90)\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n # Create nodes\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n mapping = nodes.new('ShaderNodeMapping')\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n background = nodes.new('ShaderNodeBackground')\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n # Set values\r\n mapping.inputs['Rotation'].default_value[2] = math.radians(rotation_z)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n # Link nodes\r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef set_world_color(color: tuple = (0.05, 0.05, 0.05), strength: float = 1.0) -> None:\r\n \"\"\"\r\n Set world background to a solid color.\r\n \r\n Args:\r\n color: RGB color tuple\r\n strength: Background intensity\r\n \r\n Example:\r\n >>> set_world_color((0.02, 0.02, 0.05)) # Dark blue background\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n background = nodes.new('ShaderNodeBackground')\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n background.inputs['Color'].default_value = (*color, 1.0)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n links.new(background.outputs['Background'], output.inputs['Surface'])","category":"utility","source_file":"lighting_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Mask Utilities. Functions for creating and managing masks for compositing.","output":"import bpy\r\n\r\n\r\ndef create_object_mask(\r\n obj: bpy.types.Object,\r\n pass_index: int = 1\r\n) -> None:\r\n \"\"\"\r\n Set up object for mask pass rendering.\r\n \r\n Args:\r\n obj: Object to mask\r\n pass_index: Unique pass index for the object\r\n \"\"\"\r\n obj.pass_index = pass_index\r\n bpy.context.view_layer.use_pass_object_index = True\r\n\r\n\r\ndef create_material_mask(\r\n material: bpy.types.Material,\r\n pass_index: int = 1\r\n) -> None:\r\n \"\"\"\r\n Set up material for mask pass rendering.\r\n \r\n Args:\r\n material: Material to mask\r\n pass_index: Unique pass index\r\n \"\"\"\r\n material.pass_index = pass_index\r\n bpy.context.view_layer.use_pass_material_index = True\r\n\r\n\r\ndef setup_id_mask_compositing(pass_index: int = 1) -> dict:\r\n \"\"\"\r\n Set up ID mask in compositor.\r\n \r\n Args:\r\n pass_index: Object/material pass index to mask\r\n \r\n Returns:\r\n Dictionary with compositor nodes\r\n \"\"\"\r\n tree = bpy.context.scene.node_tree\r\n nodes = tree.nodes\r\n links = tree.links\r\n \r\n # Render layers\r\n render = None\r\n for node in nodes:\r\n if node.type == 'R_LAYERS':\r\n render = node\r\n break\r\n \r\n if not render:\r\n render = nodes.new('CompositorNodeRLayers')\r\n render.location = (0, 0)\r\n \r\n # ID Mask node\r\n id_mask = nodes.new('CompositorNodeIDMask')\r\n id_mask.location = (200, -100)\r\n id_mask.index = pass_index\r\n id_mask.use_antialiasing = True\r\n \r\n links.new(render.outputs['IndexOB'], id_mask.inputs['ID value'])\r\n \r\n return {\r\n 'render': render,\r\n 'id_mask': id_mask\r\n }\r\n\r\n\r\ndef create_holdout_material(name: str = \"HoldoutMat\") -> bpy.types.Material:\r\n \"\"\"Create holdout material for masking.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n holdout = nodes.new('ShaderNodeHoldout')\r\n holdout.location = (0, 0)\r\n \r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (200, 0)\r\n \r\n links.new(holdout.outputs['Holdout'], output.inputs['Surface'])\r\n \r\n return mat\r\n\r\n\r\ndef add_cryptomatte_setup() -> dict:\r\n \"\"\"\r\n Enable and configure Cryptomatte passes.\r\n \r\n Returns:\r\n Dictionary with settings applied\r\n \"\"\"\r\n view_layer = bpy.context.view_layer\r\n view_layer.use_pass_cryptomatte_object = True\r\n view_layer.use_pass_cryptomatte_material = True\r\n view_layer.use_pass_cryptomatte_asset = True\r\n \r\n # Add cryptomatte node in compositor\r\n tree = bpy.context.scene.node_tree\r\n nodes = tree.nodes\r\n \r\n crypto = nodes.new('CompositorNodeCryptomatteV2')\r\n crypto.location = (200, -200)\r\n \r\n return {\r\n 'object_pass': True,\r\n 'material_pass': True,\r\n 'asset_pass': True,\r\n 'cryptomatte_node': crypto\r\n }\r\n\r\n\r\ndef enable_render_passes() -> None:\r\n \"\"\"Enable common render passes for compositing.\"\"\"\r\n view_layer = bpy.context.view_layer\r\n \r\n # Standard passes\r\n view_layer.use_pass_combined = True\r\n view_layer.use_pass_z = True\r\n view_layer.use_pass_mist = True\r\n view_layer.use_pass_normal = True\r\n \r\n # Light passes\r\n view_layer.use_pass_diffuse_color = True\r\n view_layer.use_pass_glossy_color = True\r\n view_layer.use_pass_emit = True\r\n \r\n # Index passes\r\n view_layer.use_pass_object_index = True\r\n view_layer.use_pass_material_index = True\r\n\r\n\r\ndef setup_mist_pass(\r\n start: float = 5.0,\r\n depth: float = 25.0,\r\n falloff: str = 'QUADRATIC'\r\n) -> None:\r\n \"\"\"\r\n Configure mist pass for depth effects.\r\n \r\n Args:\r\n start: Mist start distance\r\n depth: Mist depth\r\n falloff: 'LINEAR', 'QUADRATIC', 'INVERSE_QUADRATIC'\r\n \"\"\"\r\n bpy.context.view_layer.use_pass_mist = True\r\n \r\n world = bpy.context.scene.world\r\n if world:\r\n world.mist_settings.start = start\r\n world.mist_settings.depth = depth\r\n world.mist_settings.falloff = falloff","category":"utility","source_file":"mask_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Setup Id Mask Compositing. Category: utility","output":"import bpy\n\ndef setup_id_mask_compositing(pass_index: int = 1) -> dict:\r\n \"\"\"\r\n Set up ID mask in compositor.\r\n \r\n Args:\r\n pass_index: Object/material pass index to mask\r\n \r\n Returns:\r\n Dictionary with compositor nodes\r\n \"\"\"\r\n tree = bpy.context.scene.node_tree\r\n nodes = tree.nodes\r\n links = tree.links\r\n \r\n # Render layers\r\n render = None\r\n for node in nodes:\r\n if node.type == 'R_LAYERS':\r\n render = node\r\n break\r\n \r\n if not render:\r\n render = nodes.new('CompositorNodeRLayers')\r\n render.location = (0, 0)\r\n \r\n # ID Mask node\r\n id_mask = nodes.new('CompositorNodeIDMask')\r\n id_mask.location = (200, -100)\r\n id_mask.index = pass_index\r\n id_mask.use_antialiasing = True\r\n \r\n links.new(render.outputs['IndexOB'], id_mask.inputs['ID value'])\r\n \r\n return {\r\n 'render': render,\r\n 'id_mask': id_mask\r\n }\r\n\r\n\r\ndef create_holdout_material(name: str = \"HoldoutMat\") -> bpy.types.Material:\r\n \"\"\"Create holdout material for masking.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n holdout = nodes.new('ShaderNodeHoldout')\r\n holdout.location = (0, 0)\r\n \r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (200, 0)\r\n \r\n links.new(holdout.outputs['Holdout'], output.inputs['Surface'])\r\n \r\n return mat\r\n\r\n\r\ndef add_cryptomatte_setup() -> dict:\r\n \"\"\"\r\n Enable and configure Cryptomatte passes.\r\n \r\n Returns:\r\n Dictionary with settings applied\r\n \"\"\"\r\n view_layer = bpy.context.view_layer\r\n view_layer.use_pass_cryptomatte_object = True\r\n view_layer.use_pass_cryptomatte_material = True\r\n view_layer.use_pass_cryptomatte_asset = True\r\n \r\n # Add cryptomatte node in compositor\r\n tree = bpy.context.scene.node_tree\r\n nodes = tree.nodes\r\n \r\n crypto = nodes.new('CompositorNodeCryptomatteV2')\r\n crypto.location = (200, -200)\r\n \r\n return {\r\n 'object_pass': True,\r\n 'material_pass': True,\r\n 'asset_pass': True,\r\n 'cryptomatte_node': crypto\r\n }\r\n\r\n\r\ndef enable_render_passes() -> None:\r\n \"\"\"Enable common render passes for compositing.\"\"\"\r\n view_layer = bpy.context.view_layer\r\n \r\n # Standard passes\r\n view_layer.use_pass_combined = True\r\n view_layer.use_pass_z = True\r\n view_layer.use_pass_mist = True\r\n view_layer.use_pass_normal = True\r\n \r\n # Light passes\r\n view_layer.use_pass_diffuse_color = True\r\n view_layer.use_pass_glossy_color = True\r\n view_layer.use_pass_emit = True\r\n \r\n # Index passes\r\n view_layer.use_pass_object_index = True\r\n view_layer.use_pass_material_index = True\r\n\r\n\r\ndef setup_mist_pass(\r\n start: float = 5.0,\r\n depth: float = 25.0,\r\n falloff: str = 'QUADRATIC'\r\n) -> None:\r\n \"\"\"\r\n Configure mist pass for depth effects.\r\n \r\n Args:\r\n start: Mist start distance\r\n depth: Mist depth\r\n falloff: 'LINEAR', 'QUADRATIC', 'INVERSE_QUADRATIC'\r\n \"\"\"\r\n bpy.context.view_layer.use_pass_mist = True\r\n \r\n world = bpy.context.scene.world\r\n if world:\r\n world.mist_settings.start = start\r\n world.mist_settings.depth = depth\r\n world.mist_settings.falloff = falloff","category":"utility","source_file":"mask_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Material Presets. Pre-configured material presets for common surfaces.","output":"import bpy\r\n\r\n\r\ndef create_wood_material(\r\n name: str = \"Wood\",\r\n color: tuple = (0.35, 0.22, 0.1),\r\n grain_scale: float = 5.0,\r\n roughness: float = 0.55\r\n) -> bpy.types.Material:\r\n \"\"\"Create procedural wood material.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n # Wave texture for grain\r\n wave = nodes.new('ShaderNodeTexWave')\r\n wave.wave_type = 'BANDS'\r\n wave.inputs['Scale'].default_value = grain_scale\r\n wave.inputs['Distortion'].default_value = 8.0\r\n wave.inputs['Detail'].default_value = 2.0\r\n \r\n # Color ramp for wood tones\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.color_ramp.elements[0].color = (*[c * 0.7 for c in color], 1.0)\r\n ramp.color_ramp.elements[1].color = (*color, 1.0)\r\n \r\n links.new(wave.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n bsdf.inputs['Roughness'].default_value = roughness\r\n \r\n return mat\r\n\r\n\r\ndef create_metal_material(\r\n name: str = \"Metal\",\r\n color: tuple = (0.8, 0.8, 0.85),\r\n roughness: float = 0.3,\r\n metal_type: str = 'STEEL'\r\n) -> bpy.types.Material:\r\n \"\"\"Create metal material preset.\"\"\"\r\n colors = {\r\n 'STEEL': (0.8, 0.8, 0.85),\r\n 'GOLD': (1.0, 0.84, 0.0),\r\n 'COPPER': (0.95, 0.64, 0.54),\r\n 'BRONZE': (0.8, 0.5, 0.2),\r\n 'IRON': (0.5, 0.5, 0.5)\r\n }\r\n \r\n mat = bpy.data.materials.new(name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n final_color = colors.get(metal_type, color)\r\n bsdf.inputs['Base Color'].default_value = (*final_color, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n \r\n return mat\r\n\r\n\r\ndef create_fabric_material(\r\n name: str = \"Fabric\",\r\n color: tuple = (0.3, 0.3, 0.5),\r\n roughness: float = 0.9,\r\n sheen: float = 0.3\r\n) -> bpy.types.Material:\r\n \"\"\"Create fabric/cloth material.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n bsdf.inputs['Sheen Weight'].default_value = sheen\r\n bsdf.inputs['Sheen Tint'].default_value = 0.5\r\n \r\n return mat\r\n\r\n\r\ndef create_plastic_material(\r\n name: str = \"Plastic\",\r\n color: tuple = (0.8, 0.2, 0.2),\r\n roughness: float = 0.4,\r\n clearcoat: float = 0.5\r\n) -> bpy.types.Material:\r\n \"\"\"Create plastic material.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n bsdf.inputs['Coat Weight'].default_value = clearcoat\r\n bsdf.inputs['Coat Roughness'].default_value = 0.1\r\n \r\n return mat\r\n\r\n\r\ndef create_concrete_material(\r\n name: str = \"Concrete\",\r\n color: tuple = (0.5, 0.5, 0.48),\r\n roughness: float = 0.9\r\n) -> bpy.types.Material:\r\n \"\"\"Create concrete material with noise.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.inputs['Scale'].default_value = 50.0\r\n noise.inputs['Detail'].default_value = 10.0\r\n \r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.color_ramp.elements[0].color = (*[c * 0.85 for c in color], 1.0)\r\n ramp.color_ramp.elements[1].color = (*color, 1.0)\r\n \r\n links.new(noise.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n bsdf.inputs['Roughness'].default_value = roughness\r\n \r\n return mat\r\n\r\n\r\ndef create_brick_material(\r\n name: str = \"Brick\",\r\n brick_color: tuple = (0.6, 0.25, 0.15),\r\n mortar_color: tuple = (0.7, 0.7, 0.65),\r\n scale: float = 5.0\r\n) -> bpy.types.Material:\r\n \"\"\"Create procedural brick material.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n brick = nodes.new('ShaderNodeTexBrick')\r\n brick.inputs['Scale'].default_value = scale\r\n brick.inputs['Mortar Size'].default_value = 0.02\r\n brick.inputs['Color1'].default_value = (*brick_color, 1.0)\r\n brick.inputs['Color2'].default_value = (*[c * 0.9 for c in brick_color], 1.0)\r\n brick.inputs['Mortar'].default_value = (*mortar_color, 1.0)\r\n \r\n links.new(brick.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n \r\n return mat\r\n\r\n\r\ndef create_skin_material(\r\n name: str = \"Skin\",\r\n tone: str = 'MEDIUM',\r\n roughness: float = 0.5\r\n) -> bpy.types.Material:\r\n \"\"\"Create skin material with subsurface scattering.\"\"\"\r\n tones = {\r\n 'LIGHT': (0.95, 0.8, 0.7),\r\n 'MEDIUM': (0.8, 0.6, 0.45),\r\n 'DARK': (0.4, 0.25, 0.18)\r\n }\r\n \r\n mat = bpy.data.materials.new(name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n color = tones.get(tone, tones['MEDIUM'])\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n bsdf.inputs['Subsurface Weight'].default_value = 0.3\r\n bsdf.inputs['Subsurface Radius'].default_value = (0.1, 0.05, 0.02)\r\n \r\n return mat\r\n\r\n\r\ndef create_water_material(name: str = \"Water\") -> bpy.types.Material:\r\n \"\"\"Create water material.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n bsdf.inputs['Base Color'].default_value = (0.1, 0.4, 0.6, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.05\r\n bsdf.inputs['IOR'].default_value = 1.33\r\n bsdf.inputs['Transmission Weight'].default_value = 0.9\r\n \r\n return mat","category":"utility","source_file":"material_presets.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Create Water Material. Category: utility","output":"import bpy\n\ndef create_water_material(name: str = \"Water\") -> bpy.types.Material:\r\n \"\"\"Create water material.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n bsdf.inputs['Base Color'].default_value = (0.1, 0.4, 0.6, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.05\r\n bsdf.inputs['IOR'].default_value = 1.33\r\n bsdf.inputs['Transmission Weight'].default_value = 0.9\r\n \r\n return mat","category":"utility","source_file":"material_presets.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Material and Color Management. Creating materials, setting base colors, and assigning them to objects using the Principled BSDF shader.","output":"import bpy\r\n\r\ndef create_material(name=\"NewMaterial\", color=(1.0, 1.0, 1.0, 1.0), metallic=0.0, roughness=0.5):\r\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n if bsdf:\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Metallic'].default_value = metallic\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n \r\n return mat\r\n\r\ndef assign_material(obj, mat):\r\n if obj.data.materials:\r\n obj.data.materials[0] = mat\r\n else:\r\n obj.data.materials.append(mat)\r\n\r\n# Example: Create a blue metallic material\r\n# blue_mat = create_material(\"MetallicBlue\", (0.0, 0.0, 1.0, 1.0), metallic=1.0)\r\n# assign_material(bpy.context.active_object, blue_mat)\r\n\r\ndef create_glass_material(name=\"Glass\", color=(1.0, 1.0, 1.0, 1.0), roughness=0.0, ior=1.45):\r\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n nodes.clear()\r\n \r\n # Add Glass BSDF\r\n glass = nodes.new(type=\"ShaderNodeBsdfGlass\")\r\n glass.inputs['Color'].default_value = color\r\n glass.inputs['Roughness'].default_value = roughness\r\n glass.inputs['IOR'].default_value = ior\r\n \r\n # Add Output\r\n output = nodes.new(type=\"ShaderNodeOutputMaterial\")\r\n \r\n # Link\r\n mat.node_tree.links.new(glass.outputs['BSDF'], output.inputs['Surface'])\r\n return mat\r\n\r\ndef create_emission_material(name=\"Emission\", color=(1.0, 1.0, 1.0, 1.0), strength=1.0):\r\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n nodes.clear()\r\n \r\n # Add Emission\r\n emission = nodes.new(type=\"ShaderNodeEmission\")\r\n emission.inputs['Color'].default_value = color\r\n emission.inputs['Strength'].default_value = strength\r\n \r\n # Add Output\r\n output = nodes.new(type=\"ShaderNodeOutputMaterial\")\r\n \r\n # Link\r\n mat.node_tree.links.new(emission.outputs['Emission'], output.inputs['Surface'])\r\n return mat","category":"utility","source_file":"material_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Create Material. Category: utility","output":"import bpy\n\ndef create_material(name=\"NewMaterial\", color=(1.0, 1.0, 1.0, 1.0), metallic=0.0, roughness=0.5):\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n if bsdf:\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Metallic'].default_value = metallic\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n \r\n return mat","category":"utility","source_file":"material_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Assign Material. Category: utility","output":"import bpy\n\ndef assign_material(obj, mat):\n if obj.data.materials:\r\n obj.data.materials[0] = mat\r\n else:\r\n obj.data.materials.append(mat)","category":"utility","source_file":"material_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Create Glass Material. Category: utility","output":"import bpy\n\ndef create_glass_material(name=\"Glass\", color=(1.0, 1.0, 1.0, 1.0), roughness=0.0, ior=1.45):\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n nodes.clear()\r\n \r\n # Add Glass BSDF\r\n glass = nodes.new(type=\"ShaderNodeBsdfGlass\")\r\n glass.inputs['Color'].default_value = color\r\n glass.inputs['Roughness'].default_value = roughness\r\n glass.inputs['IOR'].default_value = ior\r\n \r\n # Add Output\r\n output = nodes.new(type=\"ShaderNodeOutputMaterial\")\r\n \r\n # Link\r\n mat.node_tree.links.new(glass.outputs['BSDF'], output.inputs['Surface'])\r\n return mat","category":"utility","source_file":"material_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Create Emission Material. Category: utility","output":"import bpy\n\ndef create_emission_material(name=\"Emission\", color=(1.0, 1.0, 1.0, 1.0), strength=1.0):\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n nodes.clear()\r\n \r\n # Add Emission\r\n emission = nodes.new(type=\"ShaderNodeEmission\")\r\n emission.inputs['Color'].default_value = color\r\n emission.inputs['Strength'].default_value = strength\r\n \r\n # Add Output\r\n output = nodes.new(type=\"ShaderNodeOutputMaterial\")\r\n \r\n # Link\r\n mat.node_tree.links.new(emission.outputs['Emission'], output.inputs['Surface'])\r\n return mat","category":"utility","source_file":"material_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Mesh Editing Utilities. Advanced mesh editing operations including bevel, extrude, bridge, fill, and subdivision.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef bevel_edges(\r\n offset: float = 0.1,\r\n segments: int = 3,\r\n profile: float = 0.5,\r\n affect: str = 'EDGES',\r\n clamp_overlap: bool = True,\r\n harden_normals: bool = False\r\n) -> None:\r\n \"\"\"\r\n Apply bevel to selected edges or vertices.\r\n \r\n Args:\r\n offset: Bevel width\r\n segments: Number of segments for curved bevel\r\n profile: Shape profile (0.5 = round, 0 = flat, 1 = pointed)\r\n affect: 'EDGES' or 'VERTICES'\r\n clamp_overlap: Prevent overlapping bevels\r\n harden_normals: Match normals to adjacent faces\r\n \r\n Example:\r\n >>> bpy.ops.object.mode_set(mode='EDIT')\r\n >>> bpy.ops.mesh.select_all(action='SELECT')\r\n >>> bevel_edges(offset=0.2, segments=4)\r\n \"\"\"\r\n bpy.ops.mesh.bevel(\r\n offset=offset,\r\n segments=segments,\r\n profile=profile,\r\n affect=affect,\r\n clamp_overlap=clamp_overlap,\r\n harden_normals=harden_normals\r\n )\r\n\r\n\r\ndef extrude_region(\r\n direction: tuple = (0, 0, 1),\r\n amount: float = 1.0\r\n) -> None:\r\n \"\"\"\r\n Extrude selected faces in a direction.\r\n \r\n Args:\r\n direction: XYZ direction vector (normalized)\r\n amount: Extrusion distance\r\n \r\n Example:\r\n >>> extrude_region((0, 0, 1), 2.0) # Extrude up\r\n \"\"\"\r\n bpy.ops.mesh.extrude_region_move(\r\n TRANSFORM_OT_translate={\r\n \"value\": tuple(d * amount for d in direction)\r\n }\r\n )\r\n\r\n\r\ndef bridge_edge_loops(\r\n segments: int = 0,\r\n smoothness: float = 1.0,\r\n interpolation: str = 'PATH'\r\n) -> None:\r\n \"\"\"\r\n Bridge two selected edge loops with faces.\r\n \r\n Args:\r\n segments: Number of intermediate cuts (0 = direct bridge)\r\n smoothness: Curvature smoothing factor\r\n interpolation: 'LINEAR', 'PATH', or 'SURFACE'\r\n \r\n Example:\r\n >>> bridge_edge_loops(segments=3, smoothness=0.5)\r\n \"\"\"\r\n bpy.ops.mesh.bridge_edge_loops(\r\n number_cuts=segments,\r\n smoothness=smoothness,\r\n interpolation=interpolation\r\n )\r\n\r\n\r\ndef fill_holes(max_sides: int = 0) -> None:\r\n \"\"\"\r\n Fill holes in the mesh.\r\n \r\n Args:\r\n max_sides: Maximum hole size to fill (0 = all holes)\r\n \r\n Example:\r\n >>> fill_holes(max_sides=8)\r\n \"\"\"\r\n bpy.ops.mesh.fill_holes(sides=max_sides)\r\n\r\n\r\ndef subdivide_mesh(\r\n cuts: int = 1,\r\n smoothness: float = 0.0,\r\n fractal: float = 0.0\r\n) -> None:\r\n \"\"\"\r\n Subdivide selected mesh faces.\r\n \r\n Args:\r\n cuts: Number of subdivision cuts\r\n smoothness: Smoothing amount\r\n fractal: Random displacement for terrain-like results\r\n \r\n Example:\r\n >>> subdivide_mesh(cuts=2, smoothness=0.5)\r\n \"\"\"\r\n bpy.ops.mesh.subdivide(\r\n number_cuts=cuts,\r\n smoothness=smoothness,\r\n fractal=fractal\r\n )\r\n\r\n\r\ndef decimate_mesh(\r\n ratio: float = 0.5,\r\n use_symmetry: bool = False,\r\n symmetry_axis: str = 'X'\r\n) -> None:\r\n \"\"\"\r\n Reduce polygon count of selected mesh.\r\n \r\n Args:\r\n ratio: Target ratio (0.5 = 50% of original)\r\n use_symmetry: Maintain symmetry on axis\r\n symmetry_axis: 'X', 'Y', or 'Z'\r\n \r\n Example:\r\n >>> decimate_mesh(ratio=0.3) # Reduce to 30%\r\n \"\"\"\r\n bpy.ops.mesh.decimate(\r\n ratio=ratio,\r\n use_symmetry=use_symmetry,\r\n symmetry_axis=symmetry_axis\r\n )\r\n\r\n\r\ndef inset_faces(\r\n thickness: float = 0.1,\r\n depth: float = 0.0,\r\n use_boundary: bool = True,\r\n use_individual: bool = False\r\n) -> None:\r\n \"\"\"\r\n Inset selected faces.\r\n \r\n Args:\r\n thickness: Inset distance\r\n depth: Depth relative to face\r\n use_boundary: Inset boundary faces\r\n use_individual: Inset each face individually\r\n \r\n Example:\r\n >>> inset_faces(thickness=0.2, depth=-0.1)\r\n \"\"\"\r\n bpy.ops.mesh.inset(\r\n thickness=thickness,\r\n depth=depth,\r\n use_boundary=use_boundary,\r\n use_individual=use_individual\r\n )\r\n\r\n\r\ndef bisect_mesh(\r\n plane_co: tuple = (0, 0, 0),\r\n plane_no: tuple = (0, 0, 1),\r\n use_fill: bool = False,\r\n clear_inner: bool = False,\r\n clear_outer: bool = False\r\n) -> None:\r\n \"\"\"\r\n Cut mesh along a plane.\r\n \r\n Args:\r\n plane_co: Point on the plane\r\n plane_no: Plane normal direction\r\n use_fill: Fill the cut with faces\r\n clear_inner: Remove geometry behind plane\r\n clear_outer: Remove geometry in front of plane\r\n \r\n Example:\r\n >>> bisect_mesh((0, 0, 1), (0, 0, 1), clear_outer=True) # Cut at Z=1\r\n \"\"\"\r\n bpy.ops.mesh.bisect(\r\n plane_co=plane_co,\r\n plane_no=plane_no,\r\n use_fill=use_fill,\r\n clear_inner=clear_inner,\r\n clear_outer=clear_outer\r\n )\r\n\r\n\r\ndef dissolve_edges(\r\n use_verts: bool = True,\r\n angle_threshold: float = None\r\n) -> None:\r\n \"\"\"\r\n Dissolve selected edges, merging adjacent faces.\r\n \r\n Args:\r\n use_verts: Also dissolve resulting 2-edge vertices\r\n angle_threshold: Preserve sharp edges above this angle (radians)\r\n \r\n Example:\r\n >>> dissolve_edges()\r\n \"\"\"\r\n kwargs = {'use_verts': use_verts}\r\n if angle_threshold is not None:\r\n kwargs['angle_threshold'] = angle_threshold\r\n bpy.ops.mesh.dissolve_edges(**kwargs)\r\n\r\n\r\ndef flip_normals() -> None:\r\n \"\"\"Flip normals of selected faces.\"\"\"\r\n bpy.ops.mesh.flip_normals()\r\n\r\n\r\ndef recalculate_normals(inside: bool = False) -> None:\r\n \"\"\"\r\n Recalculate normals to point outward (or inward).\r\n \r\n Args:\r\n inside: Point normals inward instead\r\n \"\"\"\r\n bpy.ops.mesh.normals_make_consistent(inside=inside)\r\n\r\n\r\ndef merge_vertices(\r\n type: str = 'CENTER',\r\n threshold: float = 0.0001\r\n) -> None:\r\n \"\"\"\r\n Merge selected vertices.\r\n \r\n Args:\r\n type: 'CENTER', 'CURSOR', 'COLLAPSE', 'FIRST', 'LAST'\r\n threshold: Merge by distance threshold (for auto-merge)\r\n \r\n Example:\r\n >>> merge_vertices('CENTER')\r\n \"\"\"\r\n if type in ('CENTER', 'CURSOR', 'COLLAPSE'):\r\n bpy.ops.mesh.merge(type=type)\r\n else:\r\n bpy.ops.mesh.remove_doubles(threshold=threshold)\r\n\r\n\r\ndef separate_by_selection() -> None:\r\n \"\"\"Separate selected geometry into a new object.\"\"\"\r\n bpy.ops.mesh.separate(type='SELECTED')\r\n\r\n\r\ndef separate_by_material() -> None:\r\n \"\"\"Separate geometry by material into new objects.\"\"\"\r\n bpy.ops.mesh.separate(type='MATERIAL')\r\n\r\n\r\ndef separate_by_loose() -> None:\r\n \"\"\"Separate disconnected geometry into new objects.\"\"\"\r\n bpy.ops.mesh.separate(type='LOOSE')","category":"utility","source_file":"mesh_editing_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Fill Holes. Category: utility","output":"import bpy\n\ndef fill_holes(max_sides: int = 0) -> None:\r\n \"\"\"\r\n Fill holes in the mesh.\r\n \r\n Args:\r\n max_sides: Maximum hole size to fill (0 = all holes)\r\n \r\n Example:\r\n >>> fill_holes(max_sides=8)\r\n \"\"\"\r\n bpy.ops.mesh.fill_holes(sides=max_sides)\r\n\r\n\r\ndef subdivide_mesh(\r\n cuts: int = 1,\r\n smoothness: float = 0.0,\r\n fractal: float = 0.0\r\n) -> None:\r\n \"\"\"\r\n Subdivide selected mesh faces.\r\n \r\n Args:\r\n cuts: Number of subdivision cuts\r\n smoothness: Smoothing amount\r\n fractal: Random displacement for terrain-like results\r\n \r\n Example:\r\n >>> subdivide_mesh(cuts=2, smoothness=0.5)\r\n \"\"\"\r\n bpy.ops.mesh.subdivide(\r\n number_cuts=cuts,\r\n smoothness=smoothness,\r\n fractal=fractal\r\n )\r\n\r\n\r\ndef decimate_mesh(\r\n ratio: float = 0.5,\r\n use_symmetry: bool = False,\r\n symmetry_axis: str = 'X'\r\n) -> None:\r\n \"\"\"\r\n Reduce polygon count of selected mesh.\r\n \r\n Args:\r\n ratio: Target ratio (0.5 = 50% of original)\r\n use_symmetry: Maintain symmetry on axis\r\n symmetry_axis: 'X', 'Y', or 'Z'\r\n \r\n Example:\r\n >>> decimate_mesh(ratio=0.3) # Reduce to 30%\r\n \"\"\"\r\n bpy.ops.mesh.decimate(\r\n ratio=ratio,\r\n use_symmetry=use_symmetry,\r\n symmetry_axis=symmetry_axis\r\n )\r\n\r\n\r\ndef inset_faces(\r\n thickness: float = 0.1,\r\n depth: float = 0.0,\r\n use_boundary: bool = True,\r\n use_individual: bool = False\r\n) -> None:\r\n \"\"\"\r\n Inset selected faces.\r\n \r\n Args:\r\n thickness: Inset distance\r\n depth: Depth relative to face\r\n use_boundary: Inset boundary faces\r\n use_individual: Inset each face individually\r\n \r\n Example:\r\n >>> inset_faces(thickness=0.2, depth=-0.1)\r\n \"\"\"\r\n bpy.ops.mesh.inset(\r\n thickness=thickness,\r\n depth=depth,\r\n use_boundary=use_boundary,\r\n use_individual=use_individual\r\n )\r\n\r\n\r\ndef bisect_mesh(\r\n plane_co: tuple = (0, 0, 0),\r\n plane_no: tuple = (0, 0, 1),\r\n use_fill: bool = False,\r\n clear_inner: bool = False,\r\n clear_outer: bool = False\r\n) -> None:\r\n \"\"\"\r\n Cut mesh along a plane.\r\n \r\n Args:\r\n plane_co: Point on the plane\r\n plane_no: Plane normal direction\r\n use_fill: Fill the cut with faces\r\n clear_inner: Remove geometry behind plane\r\n clear_outer: Remove geometry in front of plane\r\n \r\n Example:\r\n >>> bisect_mesh((0, 0, 1), (0, 0, 1), clear_outer=True) # Cut at Z=1\r\n \"\"\"\r\n bpy.ops.mesh.bisect(\r\n plane_co=plane_co,\r\n plane_no=plane_no,\r\n use_fill=use_fill,\r\n clear_inner=clear_inner,\r\n clear_outer=clear_outer\r\n )\r\n\r\n\r\ndef dissolve_edges(\r\n use_verts: bool = True,\r\n angle_threshold: float = None\r\n) -> None:\r\n \"\"\"\r\n Dissolve selected edges, merging adjacent faces.\r\n \r\n Args:\r\n use_verts: Also dissolve resulting 2-edge vertices\r\n angle_threshold: Preserve sharp edges above this angle (radians)\r\n \r\n Example:\r\n >>> dissolve_edges()\r\n \"\"\"\r\n kwargs = {'use_verts': use_verts}\r\n if angle_threshold is not None:\r\n kwargs['angle_threshold'] = angle_threshold\r\n bpy.ops.mesh.dissolve_edges(**kwargs)\r\n\r\n\r\ndef flip_normals() -> None:\r\n \"\"\"Flip normals of selected faces.\"\"\"\r\n bpy.ops.mesh.flip_normals()\r\n\r\n\r\ndef recalculate_normals(inside: bool = False) -> None:\r\n \"\"\"\r\n Recalculate normals to point outward (or inward).\r\n \r\n Args:\r\n inside: Point normals inward instead\r\n \"\"\"\r\n bpy.ops.mesh.normals_make_consistent(inside=inside)\r\n\r\n\r\ndef merge_vertices(\r\n type: str = 'CENTER',\r\n threshold: float = 0.0001\r\n) -> None:\r\n \"\"\"\r\n Merge selected vertices.\r\n \r\n Args:\r\n type: 'CENTER', 'CURSOR', 'COLLAPSE', 'FIRST', 'LAST'\r\n threshold: Merge by distance threshold (for auto-merge)\r\n \r\n Example:\r\n >>> merge_vertices('CENTER')\r\n \"\"\"\r\n if type in ('CENTER', 'CURSOR', 'COLLAPSE'):\n bpy.ops.mesh.merge(type=type)\r\n else:\r\n bpy.ops.mesh.remove_doubles(threshold=threshold)","category":"utility","source_file":"mesh_editing_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Separate By Selection. Category: utility","output":"import bpy\n\ndef separate_by_selection() -> None:\r\n \"\"\"Separate selected geometry into a new object.\"\"\"\r\n bpy.ops.mesh.separate(type='SELECTED')\r\n\r\n\r\ndef separate_by_material() -> None:\r\n \"\"\"Separate geometry by material into new objects.\"\"\"\r\n bpy.ops.mesh.separate(type='MATERIAL')\r\n\r\n\r\ndef separate_by_loose() -> None:\r\n \"\"\"Separate disconnected geometry into new objects.\"\"\"\r\n bpy.ops.mesh.separate(type='LOOSE')","category":"utility","source_file":"mesh_editing_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Create Basic Mesh Primitives. Standard way to add basic geometric shapes to the scene using bpy.ops.mesh.","output":"import bpy\r\nimport math\r\n\r\ndef add_cube(location=(0, 0, 0), rotation=(0, 0, 0), size=2, name=\"Cube\"):\r\n bpy.ops.mesh.primitive_cube_add(size=size, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj\r\n\r\ndef add_sphere(location=(0, 0, 0), rotation=(0, 0, 0), radius=1, segments=32, ring_count=16, name=\"Sphere\"):\r\n bpy.ops.mesh.primitive_uv_sphere_add(radius=radius, segments=segments, ring_count=ring_count, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj\r\n\r\ndef add_cylinder(location=(0, 0, 0), rotation=(0, 0, 0), radius=1, depth=2, vertices=32, name=\"Cylinder\"):\r\n bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=depth, vertices=vertices, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj\r\n\r\ndef add_plane(location=(0, 0, 0), rotation=(0, 0, 0), size=2, name=\"Plane\"):\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj\r\n\r\ndef add_torus(location=(0, 0, 0), rotation=(0, 0, 0), major_radius=1, minor_radius=0.25, major_segments=48, minor_segments=12, name=\"Torus\"):\r\n bpy.ops.mesh.primitive_torus_add(major_radius=major_radius, minor_radius=minor_radius, major_segments=major_segments, minor_segments=minor_segments, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj\r\n\r\ndef add_cone(location=(0, 0, 0), rotation=(0, 0, 0), radius1=1, radius2=0, depth=2, vertices=32, name=\"Cone\"):\r\n bpy.ops.mesh.primitive_cone_add(radius1=radius1, radius2=radius2, depth=depth, vertices=vertices, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj\r\n\r\ndef add_monkey(location=(0, 0, 0), rotation=(0, 0, 0), size=2, name=\"Suzanne\"):\r\n bpy.ops.mesh.primitive_monkey_add(size=size, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj","category":"utility","source_file":"mesh_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Add Cube. Category: utility","output":"import bpy\n\ndef add_cube(location=(0, 0, 0), rotation=(0, 0, 0), size=2, name=\"Cube\"):\n bpy.ops.mesh.primitive_cube_add(size=size, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj","category":"utility","source_file":"mesh_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Add Sphere. Category: utility","output":"import bpy\n\ndef add_sphere(location=(0, 0, 0), rotation=(0, 0, 0), radius=1, segments=32, ring_count=16, name=\"Sphere\"):\n bpy.ops.mesh.primitive_uv_sphere_add(radius=radius, segments=segments, ring_count=ring_count, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj","category":"utility","source_file":"mesh_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Add Cylinder. Category: utility","output":"import bpy\n\ndef add_cylinder(location=(0, 0, 0), rotation=(0, 0, 0), radius=1, depth=2, vertices=32, name=\"Cylinder\"):\n bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=depth, vertices=vertices, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj","category":"utility","source_file":"mesh_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Add Plane. Category: utility","output":"import bpy\n\ndef add_plane(location=(0, 0, 0), rotation=(0, 0, 0), size=2, name=\"Plane\"):\n bpy.ops.mesh.primitive_plane_add(size=size, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj","category":"utility","source_file":"mesh_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Add Torus. Category: utility","output":"import bpy\n\ndef add_torus(location=(0, 0, 0), rotation=(0, 0, 0), major_radius=1, minor_radius=0.25, major_segments=48, minor_segments=12, name=\"Torus\"):\n bpy.ops.mesh.primitive_torus_add(major_radius=major_radius, minor_radius=minor_radius, major_segments=major_segments, minor_segments=minor_segments, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj","category":"utility","source_file":"mesh_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Add Cone. Category: utility","output":"import bpy\n\ndef add_cone(location=(0, 0, 0), rotation=(0, 0, 0), radius1=1, radius2=0, depth=2, vertices=32, name=\"Cone\"):\n bpy.ops.mesh.primitive_cone_add(radius1=radius1, radius2=radius2, depth=depth, vertices=vertices, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj","category":"utility","source_file":"mesh_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Add Monkey. Category: utility","output":"import bpy\n\ndef add_monkey(location=(0, 0, 0), rotation=(0, 0, 0), size=2, name=\"Suzanne\"):\n bpy.ops.mesh.primitive_monkey_add(size=size, location=location, rotation=tuple(math.radians(d) for d in rotation))\r\n obj = bpy.context.active_object\r\n obj.name = name\r\n return obj","category":"utility","source_file":"mesh_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Modifier Utilities. Functions for adding and configuring mesh modifiers.","output":"import bpy\r\n\r\n\r\ndef add_subdivision(\r\n obj: bpy.types.Object,\r\n levels: int = 2,\r\n render_levels: int = None,\r\n use_limit_surface: bool = True\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add subdivision surface modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Subdivision\", 'SUBSURF')\r\n mod.levels = levels\r\n mod.render_levels = render_levels or levels\r\n mod.use_limit_surface = use_limit_surface\r\n return mod\r\n\r\n\r\ndef add_mirror(\r\n obj: bpy.types.Object,\r\n axis: str = 'X',\r\n use_clip: bool = True,\r\n merge_threshold: float = 0.001\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add mirror modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Mirror\", 'MIRROR')\r\n mod.use_axis[0] = 'X' in axis.upper()\r\n mod.use_axis[1] = 'Y' in axis.upper()\r\n mod.use_axis[2] = 'Z' in axis.upper()\r\n mod.use_clip = use_clip\r\n mod.merge_threshold = merge_threshold\r\n return mod\r\n\r\n\r\ndef add_array(\r\n obj: bpy.types.Object,\r\n count: int = 3,\r\n offset: tuple = (1.1, 0, 0),\r\n use_relative: bool = True\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add array modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Array\", 'ARRAY')\r\n mod.count = count\r\n mod.use_relative_offset = use_relative\r\n if use_relative:\r\n mod.relative_offset_displace = offset\r\n else:\r\n mod.use_constant_offset = True\r\n mod.constant_offset_displace = offset\r\n return mod\r\n\r\n\r\ndef add_bevel_modifier(\r\n obj: bpy.types.Object,\r\n width: float = 0.02,\r\n segments: int = 3,\r\n limit_method: str = 'ANGLE',\r\n angle_limit: float = 30\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add bevel modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Bevel\", 'BEVEL')\r\n mod.width = width\r\n mod.segments = segments\r\n mod.limit_method = limit_method\r\n if limit_method == 'ANGLE':\r\n import math\r\n mod.angle_limit = math.radians(angle_limit)\r\n return mod\r\n\r\n\r\ndef add_solidify(\r\n obj: bpy.types.Object,\r\n thickness: float = 0.05,\r\n offset: float = -1.0,\r\n use_even_offset: bool = True\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add solidify modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Solidify\", 'SOLIDIFY')\r\n mod.thickness = thickness\r\n mod.offset = offset\r\n mod.use_even_offset = use_even_offset\r\n return mod\r\n\r\n\r\ndef add_boolean(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n operation: str = 'DIFFERENCE',\r\n solver: str = 'EXACT'\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add boolean modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Boolean\", 'BOOLEAN')\r\n mod.operation = operation # 'DIFFERENCE', 'UNION', 'INTERSECT'\r\n mod.object = target\r\n mod.solver = solver\r\n return mod\r\n\r\n\r\ndef add_displace(\r\n obj: bpy.types.Object,\r\n strength: float = 0.5,\r\n mid_level: float = 0.5,\r\n texture_type: str = 'CLOUDS'\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add displacement modifier with procedural texture.\"\"\"\r\n tex = bpy.data.textures.new(\"DisplaceTex\", type=texture_type)\r\n mod = obj.modifiers.new(\"Displace\", 'DISPLACE')\r\n mod.texture = tex\r\n mod.strength = strength\r\n mod.mid_level = mid_level\r\n return mod\r\n\r\n\r\ndef add_shrinkwrap(\r\n obj: bpy.types.Object,\r\n target: bpy.types.Object,\r\n wrap_method: str = 'NEAREST_SURFACEPOINT',\r\n offset: float = 0.0\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add shrinkwrap modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Shrinkwrap\", 'SHRINKWRAP')\r\n mod.target = target\r\n mod.wrap_method = wrap_method\r\n mod.offset = offset\r\n return mod\r\n\r\n\r\ndef add_decimate(\r\n obj: bpy.types.Object,\r\n ratio: float = 0.5,\r\n decimate_type: str = 'COLLAPSE'\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add decimate modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Decimate\", 'DECIMATE')\r\n mod.decimate_type = decimate_type\r\n if decimate_type == 'COLLAPSE':\r\n mod.ratio = ratio\r\n return mod\r\n\r\n\r\ndef add_edge_split(\r\n obj: bpy.types.Object,\r\n split_angle: float = 30,\r\n use_edge_sharp: bool = True\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add edge split modifier.\"\"\"\r\n import math\r\n mod = obj.modifiers.new(\"EdgeSplit\", 'EDGE_SPLIT')\r\n mod.split_angle = math.radians(split_angle)\r\n mod.use_edge_sharp = use_edge_sharp\r\n return mod\r\n\r\n\r\ndef add_remesh(\r\n obj: bpy.types.Object,\r\n mode: str = 'VOXEL',\r\n voxel_size: float = 0.1,\r\n octree_depth: int = 4\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add remesh modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Remesh\", 'REMESH')\r\n mod.mode = mode\r\n if mode == 'VOXEL':\r\n mod.voxel_size = voxel_size\r\n else:\r\n mod.octree_depth = octree_depth\r\n return mod\r\n\r\n\r\ndef add_smooth(\r\n obj: bpy.types.Object,\r\n factor: float = 0.5,\r\n iterations: int = 1\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add smooth modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Smooth\", 'SMOOTH')\r\n mod.factor = factor\r\n mod.iterations = iterations\r\n return mod\r\n\r\n\r\ndef add_skin(obj: bpy.types.Object) -> bpy.types.Modifier:\r\n \"\"\"Add skin modifier for quick mesh generation.\"\"\"\r\n mod = obj.modifiers.new(\"Skin\", 'SKIN')\r\n return mod\r\n\r\n\r\ndef add_wireframe(\r\n obj: bpy.types.Object,\r\n thickness: float = 0.02,\r\n use_even_offset: bool = True\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add wireframe modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Wireframe\", 'WIREFRAME')\r\n mod.thickness = thickness\r\n mod.use_even_offset = use_even_offset\r\n return mod\r\n\r\n\r\ndef apply_modifier(obj: bpy.types.Object, modifier_name: str) -> None:\r\n \"\"\"Apply a specific modifier.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.modifier_apply(modifier=modifier_name)\r\n\r\n\r\ndef apply_all_modifiers(obj: bpy.types.Object) -> None:\r\n \"\"\"Apply all modifiers on object.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n for mod in obj.modifiers[:]:\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=mod.name)\r\n except:\r\n pass","category":"utility","source_file":"modifier_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Add Skin. Category: utility","output":"import bpy\n\ndef add_skin(obj: bpy.types.Object) -> bpy.types.Modifier:\r\n \"\"\"Add skin modifier for quick mesh generation.\"\"\"\r\n mod = obj.modifiers.new(\"Skin\", 'SKIN')\r\n return mod\r\n\r\n\r\ndef add_wireframe(\r\n obj: bpy.types.Object,\r\n thickness: float = 0.02,\r\n use_even_offset: bool = True\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add wireframe modifier.\"\"\"\r\n mod = obj.modifiers.new(\"Wireframe\", 'WIREFRAME')\r\n mod.thickness = thickness\r\n mod.use_even_offset = use_even_offset\r\n return mod\r\n\r\n\r\ndef apply_modifier(obj: bpy.types.Object, modifier_name: str) -> None:\r\n \"\"\"Apply a specific modifier.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.modifier_apply(modifier=modifier_name)\r\n\r\n\r\ndef apply_all_modifiers(obj: bpy.types.Object) -> None:\r\n \"\"\"Apply all modifiers on object.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n for mod in obj.modifiers[:]:\r\n try:\r\n bpy.ops.object.modifier_apply(modifier=mod.name)\r\n except:\r\n pass","category":"utility","source_file":"modifier_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Node Material Utilities. Functions for creating shader node setups and PBR materials.","output":"import bpy\r\n\r\n\r\ndef create_pbr_material(\r\n name: str = \"PBR_Material\",\r\n base_color: tuple = (0.8, 0.8, 0.8),\r\n metallic: float = 0.0,\r\n roughness: float = 0.5,\r\n emission_color: tuple = None,\r\n emission_strength: float = 0.0\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Create a basic PBR material.\r\n \r\n Args:\r\n name: Material name\r\n base_color: RGB base color\r\n metallic: Metallic value (0-1)\r\n roughness: Roughness value (0-1)\r\n emission_color: RGB emission color\r\n emission_strength: Emission intensity\r\n \r\n Returns:\r\n The created material\r\n \"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*base_color, 1.0)\r\n bsdf.inputs['Metallic'].default_value = metallic\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n \r\n if emission_color and emission_strength > 0:\r\n bsdf.inputs['Emission Color'].default_value = (*emission_color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = emission_strength\r\n \r\n return mat\r\n\r\n\r\ndef create_glass_material(\r\n name: str = \"Glass\",\r\n color: tuple = (1, 1, 1),\r\n ior: float = 1.45,\r\n roughness: float = 0.0\r\n) -> bpy.types.Material:\r\n \"\"\"Create glass/transparent material.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n bsdf.inputs['IOR'].default_value = ior\r\n bsdf.inputs['Transmission Weight'].default_value = 1.0\r\n \r\n mat.blend_method = 'BLEND'\r\n return mat\r\n\r\n\r\ndef create_metal_material(\r\n name: str = \"Metal\",\r\n color: tuple = (0.8, 0.8, 0.85),\r\n roughness: float = 0.3\r\n) -> bpy.types.Material:\r\n \"\"\"Create metallic material.\"\"\"\r\n mat = create_pbr_material(\r\n name=name,\r\n base_color=color,\r\n metallic=1.0,\r\n roughness=roughness\r\n )\r\n return mat\r\n\r\n\r\ndef create_emission_material(\r\n name: str = \"Emission\",\r\n color: tuple = (1, 1, 1),\r\n strength: float = 10.0\r\n) -> bpy.types.Material:\r\n \"\"\"Create emissive/glow material.\"\"\"\r\n mat = create_pbr_material(\r\n name=name,\r\n base_color=color,\r\n emission_color=color,\r\n emission_strength=strength\r\n )\r\n return mat\r\n\r\n\r\ndef add_image_texture(\r\n material: bpy.types.Material,\r\n image_path: str,\r\n connection: str = 'BASE_COLOR'\r\n) -> bpy.types.ShaderNodeTexImage:\r\n \"\"\"\r\n Add image texture to material.\r\n \r\n Args:\r\n material: Target material\r\n image_path: Path to image file\r\n connection: 'BASE_COLOR', 'ROUGHNESS', 'NORMAL', 'METALLIC'\r\n \r\n Returns:\r\n The texture node\r\n \"\"\"\r\n nodes = material.node_tree.nodes\r\n links = material.node_tree.links\r\n \r\n tex = nodes.new('ShaderNodeTexImage')\r\n tex.image = bpy.data.images.load(image_path)\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n connections = {\r\n 'BASE_COLOR': 'Base Color',\r\n 'ROUGHNESS': 'Roughness',\r\n 'METALLIC': 'Metallic',\r\n }\r\n \r\n if connection == 'NORMAL':\r\n normal_map = nodes.new('ShaderNodeNormalMap')\r\n links.new(tex.outputs['Color'], normal_map.inputs['Color'])\r\n links.new(normal_map.outputs['Normal'], bsdf.inputs['Normal'])\r\n elif connection in connections:\r\n links.new(tex.outputs['Color'], bsdf.inputs[connections[connection]])\r\n \r\n return tex\r\n\r\n\r\ndef create_procedural_noise_material(\r\n name: str = \"ProceduralNoise\",\r\n color1: tuple = (0.2, 0.2, 0.2),\r\n color2: tuple = (0.8, 0.8, 0.8),\r\n scale: float = 5.0,\r\n detail: float = 2.0\r\n) -> bpy.types.Material:\r\n \"\"\"Create material with procedural noise texture.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n # Noise texture\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.inputs['Scale'].default_value = scale\r\n noise.inputs['Detail'].default_value = detail\r\n \r\n # Color ramp\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.color_ramp.elements[0].color = (*color1, 1.0)\r\n ramp.color_ramp.elements[1].color = (*color2, 1.0)\r\n \r\n links.new(noise.outputs['Fac'], ramp.inputs['Fac'])\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n return mat\r\n\r\n\r\ndef create_gradient_material(\r\n name: str = \"Gradient\",\r\n color1: tuple = (0, 0, 0),\r\n color2: tuple = (1, 1, 1),\r\n gradient_type: str = 'LINEAR'\r\n) -> bpy.types.Material:\r\n \"\"\"Create gradient material.\"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n coord = nodes.new('ShaderNodeTexCoord')\r\n mapping = nodes.new('ShaderNodeMapping')\r\n gradient = nodes.new('ShaderNodeTexGradient')\r\n gradient.gradient_type = gradient_type\r\n \r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.color_ramp.elements[0].color = (*color1, 1.0)\r\n ramp.color_ramp.elements[1].color = (*color2, 1.0)\r\n \r\n links.new(coord.outputs['UV'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], gradient.inputs['Vector'])\r\n links.new(gradient.outputs['Fac'], ramp.inputs['Fac'])\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n return mat\r\n\r\n\r\ndef assign_material(obj: bpy.types.Object, material: bpy.types.Material) -> None:\r\n \"\"\"Assign material to object.\"\"\"\r\n if obj.data.materials:\r\n obj.data.materials[0] = material\r\n else:\r\n obj.data.materials.append(material)","category":"utility","source_file":"node_material_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Assign Material. Category: utility","output":"import bpy\n\ndef assign_material(obj: bpy.types.Object, material: bpy.types.Material) -> None:\r\n \"\"\"Assign material to object.\"\"\"\r\n if obj.data.materials:\r\n obj.data.materials[0] = material\r\n else:\r\n obj.data.materials.append(material)","category":"utility","source_file":"node_material_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Object Operations Utilities. Object-level operations including duplication, joining, parenting, origin manipulation, and constraints.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef duplicate_object(\r\n obj: bpy.types.Object = None,\r\n linked: bool = False,\r\n offset: tuple = (0, 0, 0)\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Duplicate an object.\r\n \r\n Args:\r\n obj: Object to duplicate (uses active if None)\r\n linked: Create linked duplicate (shares mesh data)\r\n offset: Position offset from original\r\n \r\n Returns:\r\n The duplicated object\r\n \r\n Example:\r\n >>> new_obj = duplicate_object(cube, offset=(2, 0, 0))\r\n \"\"\"\r\n if obj:\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n \r\n bpy.ops.object.duplicate(linked=linked)\r\n duplicate = bpy.context.active_object\r\n duplicate.location.x += offset[0]\r\n duplicate.location.y += offset[1]\r\n duplicate.location.z += offset[2]\r\n \r\n return duplicate\r\n\r\n\r\ndef join_objects(objects: list) -> bpy.types.Object:\r\n \"\"\"\r\n Join multiple objects into one.\r\n \r\n Args:\r\n objects: List of objects to join\r\n \r\n Returns:\r\n The resulting joined object\r\n \r\n Example:\r\n >>> result = join_objects([cube1, cube2, cube3])\r\n \"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n for obj in objects:\r\n obj.select_set(True)\r\n \r\n bpy.context.view_layer.objects.active = objects[0]\r\n bpy.ops.object.join()\r\n \r\n return bpy.context.active_object\r\n\r\n\r\ndef set_parent(\r\n child: bpy.types.Object,\r\n parent: bpy.types.Object,\r\n keep_transform: bool = True\r\n) -> None:\r\n \"\"\"\r\n Parent one object to another.\r\n \r\n Args:\r\n child: Object to parent\r\n parent: Parent object\r\n keep_transform: Maintain child's world transform\r\n \r\n Example:\r\n >>> set_parent(wheel, car_body)\r\n \"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n child.select_set(True)\r\n parent.select_set(True)\r\n bpy.context.view_layer.objects.active = parent\r\n \r\n if keep_transform:\r\n bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)\r\n else:\r\n bpy.ops.object.parent_set(type='OBJECT', keep_transform=False)\r\n\r\n\r\ndef clear_parent(obj: bpy.types.Object, keep_transform: bool = True) -> None:\r\n \"\"\"\r\n Remove parent from object.\r\n \r\n Args:\r\n obj: Object to unparent\r\n keep_transform: Maintain world transform\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM' if keep_transform else 'CLEAR')\r\n\r\n\r\ndef set_origin(\r\n obj: bpy.types.Object,\r\n origin_type: str = 'GEOMETRY'\r\n) -> None:\r\n \"\"\"\r\n Set object origin point.\r\n \r\n Args:\r\n obj: Target object\r\n origin_type: 'GEOMETRY' (center of mesh), 'CURSOR' (3D cursor),\r\n 'CENTER_OF_MASS' (based on volume), 'CENTER_OF_VOLUME'\r\n \r\n Example:\r\n >>> set_origin(cube, 'GEOMETRY')\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n \r\n type_map = {\r\n 'GEOMETRY': 'ORIGIN_GEOMETRY',\r\n 'CURSOR': 'ORIGIN_CURSOR',\r\n 'CENTER_OF_MASS': 'ORIGIN_CENTER_OF_MASS',\r\n 'CENTER_OF_VOLUME': 'ORIGIN_CENTER_OF_VOLUME'\r\n }\r\n \r\n bpy.ops.object.origin_set(type=type_map.get(origin_type, 'ORIGIN_GEOMETRY'))\r\n\r\n\r\ndef apply_transforms(\r\n obj: bpy.types.Object,\r\n location: bool = False,\r\n rotation: bool = True,\r\n scale: bool = True\r\n) -> None:\r\n \"\"\"\r\n Apply object transforms to mesh data.\r\n \r\n Args:\r\n obj: Target object\r\n location: Apply location (move to origin)\r\n rotation: Apply rotation\r\n scale: Apply scale\r\n \r\n Example:\r\n >>> apply_transforms(obj, rotation=True, scale=True)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.transform_apply(location=location, rotation=rotation, scale=scale)\r\n\r\n\r\ndef convert_to_mesh(obj: bpy.types.Object, keep_original: bool = False) -> bpy.types.Object:\r\n \"\"\"\r\n Convert curve, text, or other object to mesh.\r\n \r\n Args:\r\n obj: Object to convert\r\n keep_original: Keep the original object\r\n \r\n Returns:\r\n The converted mesh object\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.convert(target='MESH', keep_original=keep_original)\r\n return bpy.context.active_object\r\n\r\n\r\ndef add_constraint(\r\n obj: bpy.types.Object,\r\n constraint_type: str,\r\n target: bpy.types.Object = None,\r\n **kwargs\r\n) -> bpy.types.Constraint:\r\n \"\"\"\r\n Add a constraint to an object.\r\n \r\n Args:\r\n obj: Object to constrain\r\n constraint_type: 'COPY_LOCATION', 'COPY_ROTATION', 'COPY_SCALE',\r\n 'TRACK_TO', 'FOLLOW_PATH', 'LIMIT_LOCATION', etc.\r\n target: Target object for the constraint\r\n **kwargs: Additional constraint properties\r\n \r\n Returns:\r\n The created constraint\r\n \r\n Example:\r\n >>> add_constraint(camera, 'TRACK_TO', target=subject)\r\n \"\"\"\r\n constraint = obj.constraints.new(type=constraint_type)\r\n \r\n if target and hasattr(constraint, 'target'):\r\n constraint.target = target\r\n \r\n for key, value in kwargs.items():\r\n if hasattr(constraint, key):\r\n setattr(constraint, key, value)\r\n \r\n return constraint\r\n\r\n\r\ndef copy_transforms(source: bpy.types.Object, target: bpy.types.Object) -> None:\r\n \"\"\"\r\n Copy transforms from one object to another.\r\n \r\n Args:\r\n source: Object to copy from\r\n target: Object to copy to\r\n \"\"\"\r\n target.location = source.location.copy()\r\n target.rotation_euler = source.rotation_euler.copy()\r\n target.scale = source.scale.copy()\r\n\r\n\r\ndef make_single_user(obj: bpy.types.Object, data: bool = True, materials: bool = True) -> None:\r\n \"\"\"\r\n Make object data single-user (unlink from other objects).\r\n \r\n Args:\r\n obj: Object to make single-user\r\n data: Make mesh/curve data single-user\r\n materials: Make materials single-user\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.make_single_user(\r\n type='SELECTED_OBJECTS',\r\n object=True,\r\n obdata=data,\r\n material=materials\r\n )\r\n\r\n\r\ndef shade_smooth(obj: bpy.types.Object) -> None:\r\n \"\"\"Set object to smooth shading.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.shade_smooth()\r\n\r\n\r\ndef shade_flat(obj: bpy.types.Object) -> None:\r\n \"\"\"Set object to flat shading.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.shade_flat()\r\n\r\n\r\ndef add_empty(\r\n location: tuple = (0, 0, 0),\r\n empty_type: str = 'PLAIN_AXES',\r\n size: float = 1.0,\r\n name: str = \"Empty\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add an empty object.\r\n \r\n Args:\r\n location: Position\r\n empty_type: 'PLAIN_AXES', 'ARROWS', 'CIRCLE', 'CUBE', 'SPHERE', 'CONE'\r\n size: Display size\r\n name: Object name\r\n \r\n Returns:\r\n The created empty\r\n \"\"\"\r\n bpy.ops.object.empty_add(type=empty_type, location=location, radius=size)\r\n empty = bpy.context.active_object\r\n empty.name = name\r\n return empty\r\n\r\n\r\ndef group_objects(objects: list, empty_name: str = \"Group\") -> bpy.types.Object:\r\n \"\"\"\r\n Group objects under an empty parent.\r\n \r\n Args:\r\n objects: Objects to group\r\n empty_name: Name for the group empty\r\n \r\n Returns:\r\n The group empty object\r\n \"\"\"\r\n # Calculate center\r\n center = [0, 0, 0]\r\n for obj in objects:\r\n center[0] += obj.location.x\r\n center[1] += obj.location.y\r\n center[2] += obj.location.z\r\n \r\n n = len(objects)\r\n center = (center[0]/n, center[1]/n, center[2]/n)\r\n \r\n # Create empty at center\r\n group = add_empty(location=center, name=empty_name)\r\n \r\n # Parent all objects to empty\r\n for obj in objects:\r\n set_parent(obj, group, keep_transform=True)\r\n \r\n return group","category":"utility","source_file":"object_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Join Objects. Category: utility","output":"import bpy\n\ndef join_objects(objects: list) -> bpy.types.Object:\r\n \"\"\"\r\n Join multiple objects into one.\r\n \r\n Args:\r\n objects: List of objects to join\r\n \r\n Returns:\r\n The resulting joined object\r\n \r\n Example:\r\n >>> result = join_objects([cube1, cube2, cube3])\r\n \"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n for obj in objects:\r\n obj.select_set(True)\r\n \r\n bpy.context.view_layer.objects.active = objects[0]\r\n bpy.ops.object.join()\r\n \r\n return bpy.context.active_object\r\n\r\n\r\ndef set_parent(\r\n child: bpy.types.Object,\r\n parent: bpy.types.Object,\r\n keep_transform: bool = True\r\n) -> None:\r\n \"\"\"\r\n Parent one object to another.\r\n \r\n Args:\r\n child: Object to parent\r\n parent: Parent object\r\n keep_transform: Maintain child's world transform\r\n \r\n Example:\r\n >>> set_parent(wheel, car_body)\r\n \"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n child.select_set(True)\r\n parent.select_set(True)\r\n bpy.context.view_layer.objects.active = parent\r\n \r\n if keep_transform:\r\n bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)\r\n else:\r\n bpy.ops.object.parent_set(type='OBJECT', keep_transform=False)\r\n\r\n\r\ndef clear_parent(obj: bpy.types.Object, keep_transform: bool = True) -> None:\r\n \"\"\"\r\n Remove parent from object.\r\n \r\n Args:\r\n obj: Object to unparent\r\n keep_transform: Maintain world transform\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM' if keep_transform else 'CLEAR')\r\n\r\n\r\ndef set_origin(\r\n obj: bpy.types.Object,\r\n origin_type: str = 'GEOMETRY'\r\n) -> None:\r\n \"\"\"\r\n Set object origin point.\r\n \r\n Args:\r\n obj: Target object\r\n origin_type: 'GEOMETRY' (center of mesh), 'CURSOR' (3D cursor),\r\n 'CENTER_OF_MASS' (based on volume), 'CENTER_OF_VOLUME'\r\n \r\n Example:\r\n >>> set_origin(cube, 'GEOMETRY')\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n \r\n type_map = {\r\n 'GEOMETRY': 'ORIGIN_GEOMETRY',\r\n 'CURSOR': 'ORIGIN_CURSOR',\r\n 'CENTER_OF_MASS': 'ORIGIN_CENTER_OF_MASS',\r\n 'CENTER_OF_VOLUME': 'ORIGIN_CENTER_OF_VOLUME'\r\n }\r\n \r\n bpy.ops.object.origin_set(type=type_map.get(origin_type, 'ORIGIN_GEOMETRY'))\r\n\r\n\r\ndef apply_transforms(\r\n obj: bpy.types.Object,\r\n location: bool = False,\r\n rotation: bool = True,\r\n scale: bool = True\r\n) -> None:\r\n \"\"\"\r\n Apply object transforms to mesh data.\r\n \r\n Args:\r\n obj: Target object\r\n location: Apply location (move to origin)\r\n rotation: Apply rotation\r\n scale: Apply scale\r\n \r\n Example:\r\n >>> apply_transforms(obj, rotation=True, scale=True)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.transform_apply(location=location, rotation=rotation, scale=scale)\r\n\r\n\r\ndef convert_to_mesh(obj: bpy.types.Object, keep_original: bool = False) -> bpy.types.Object:\r\n \"\"\"\r\n Convert curve, text, or other object to mesh.\r\n \r\n Args:\r\n obj: Object to convert\r\n keep_original: Keep the original object\r\n \r\n Returns:\r\n The converted mesh object\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.convert(target='MESH', keep_original=keep_original)\r\n return bpy.context.active_object\r\n\r\n\r\ndef add_constraint(\r\n obj: bpy.types.Object,\r\n constraint_type: str,\r\n target: bpy.types.Object = None,\r\n **kwargs\r\n) -> bpy.types.Constraint:\r\n \"\"\"\r\n Add a constraint to an object.\r\n \r\n Args:\r\n obj: Object to constrain\r\n constraint_type: 'COPY_LOCATION', 'COPY_ROTATION', 'COPY_SCALE',\r\n 'TRACK_TO', 'FOLLOW_PATH', 'LIMIT_LOCATION', etc.\r\n target: Target object for the constraint\r\n **kwargs: Additional constraint properties\r\n \r\n Returns:\r\n The created constraint\r\n \r\n Example:\r\n >>> add_constraint(camera, 'TRACK_TO', target=subject)\r\n \"\"\"\r\n constraint = obj.constraints.new(type=constraint_type)\r\n \r\n if target and hasattr(constraint, 'target'):\n constraint.target = target\r\n \r\n for key, value in kwargs.items():\r\n if hasattr(constraint, key):\r\n setattr(constraint, key, value)\r\n \r\n return constraint","category":"utility","source_file":"object_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Copy Transforms. Category: utility","output":"import bpy\n\ndef copy_transforms(source: bpy.types.Object, target: bpy.types.Object) -> None:\r\n \"\"\"\r\n Copy transforms from one object to another.\r\n \r\n Args:\r\n source: Object to copy from\r\n target: Object to copy to\r\n \"\"\"\r\n target.location = source.location.copy()\r\n target.rotation_euler = source.rotation_euler.copy()\r\n target.scale = source.scale.copy()\r\n\r\n\r\ndef make_single_user(obj: bpy.types.Object, data: bool = True, materials: bool = True) -> None:\r\n \"\"\"\r\n Make object data single-user (unlink from other objects).\r\n \r\n Args:\r\n obj: Object to make single-user\r\n data: Make mesh/curve data single-user\r\n materials: Make materials single-user\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n bpy.ops.object.make_single_user(\r\n type='SELECTED_OBJECTS',\r\n object=True,\r\n obdata=data,\r\n material=materials\r\n )\r\n\r\n\r\ndef shade_smooth(obj: bpy.types.Object) -> None:\r\n \"\"\"Set object to smooth shading.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.shade_smooth()\r\n\r\n\r\ndef shade_flat(obj: bpy.types.Object) -> None:\r\n \"\"\"Set object to flat shading.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.shade_flat()\r\n\r\n\r\ndef add_empty(\r\n location: tuple = (0, 0, 0),\r\n empty_type: str = 'PLAIN_AXES',\r\n size: float = 1.0,\r\n name: str = \"Empty\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add an empty object.\r\n \r\n Args:\r\n location: Position\r\n empty_type: 'PLAIN_AXES', 'ARROWS', 'CIRCLE', 'CUBE', 'SPHERE', 'CONE'\r\n size: Display size\r\n name: Object name\r\n \r\n Returns:\r\n The created empty\r\n \"\"\"\r\n bpy.ops.object.empty_add(type=empty_type, location=location, radius=size)\r\n empty = bpy.context.active_object\r\n empty.name = name\r\n return empty\r\n\r\n\r\ndef group_objects(objects: list, empty_name: str = \"Group\") -> bpy.types.Object:\r\n \"\"\"\r\n Group objects under an empty parent.\r\n \r\n Args:\r\n objects: Objects to group\r\n empty_name: Name for the group empty\r\n \r\n Returns:\r\n The group empty object\r\n \"\"\"\r\n # Calculate center\r\n center = [0, 0, 0]\r\n for obj in objects:\r\n center[0] += obj.location.x\r\n center[1] += obj.location.y\r\n center[2] += obj.location.z\r\n \r\n n = len(objects)\r\n center = (center[0]/n, center[1]/n, center[2]/n)\r\n \r\n # Create empty at center\r\n group = add_empty(location=center, name=empty_name)\r\n \r\n # Parent all objects to empty\r\n for obj in objects:\r\n set_parent(obj, group, keep_transform=True)\r\n \r\n return group","category":"utility","source_file":"object_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Particle System Utilities. Functions for creating and configuring particle systems including hair and emitter types.","output":"import bpy\r\n\r\n\r\ndef add_particle_system(\r\n obj: bpy.types.Object,\r\n name: str = \"ParticleSystem\",\r\n particle_type: str = 'EMITTER'\r\n) -> tuple:\r\n \"\"\"\r\n Add a particle system to an object.\r\n \r\n Args:\r\n obj: Object to add particles to\r\n name: Name for the particle system\r\n particle_type: 'EMITTER' or 'HAIR'\r\n \r\n Returns:\r\n Tuple of (particle_system, particle_settings)\r\n \r\n Example:\r\n >>> ps, settings = add_particle_system(plane, \"Rain\", \"EMITTER\")\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.particle_system_add()\r\n \r\n particle_sys = obj.particle_systems[-1]\r\n particle_sys.name = name\r\n settings = particle_sys.settings\r\n settings.name = f\"{name}_Settings\"\r\n settings.type = particle_type\r\n \r\n return particle_sys, settings\r\n\r\n\r\ndef configure_emitter(\r\n settings: bpy.types.ParticleSettings,\r\n count: int = 1000,\r\n lifetime: int = 50,\r\n frame_start: int = 1,\r\n frame_end: int = 200,\r\n emit_from: str = 'FACE',\r\n velocity_normal: float = 1.0,\r\n velocity_random: float = 0.0,\r\n size: float = 0.05,\r\n size_random: float = 0.0\r\n) -> None:\r\n \"\"\"\r\n Configure emitter particle settings.\r\n \r\n Args:\r\n settings: Particle settings object\r\n count: Number of particles\r\n lifetime: Particle lifetime in frames\r\n frame_start: Emission start frame\r\n frame_end: Emission end frame\r\n emit_from: 'VERT', 'FACE', 'VOLUME'\r\n velocity_normal: Velocity along normals\r\n velocity_random: Random velocity factor\r\n size: Particle size\r\n size_random: Random size variation (0-1)\r\n \r\n Example:\r\n >>> configure_emitter(settings, count=5000, lifetime=100)\r\n \"\"\"\r\n settings.type = 'EMITTER'\r\n settings.count = count\r\n settings.lifetime = lifetime\r\n settings.frame_start = frame_start\r\n settings.frame_end = frame_end\r\n settings.emit_from = emit_from\r\n settings.normal_factor = velocity_normal\r\n settings.factor_random = velocity_random\r\n settings.particle_size = size\r\n settings.size_random = size_random\r\n\r\n\r\ndef configure_hair(\r\n settings: bpy.types.ParticleSettings,\r\n count: int = 1000,\r\n hair_length: float = 0.2,\r\n segments: int = 5,\r\n emit_from: str = 'FACE',\r\n use_advanced: bool = True\r\n) -> None:\r\n \"\"\"\r\n Configure hair particle settings.\r\n \r\n Args:\r\n settings: Particle settings object\r\n count: Number of hair strands\r\n hair_length: Length of each strand\r\n segments: Segments per strand\r\n emit_from: 'VERT', 'FACE'\r\n use_advanced: Enable advanced hair settings\r\n \r\n Example:\r\n >>> configure_hair(settings, count=10000, hair_length=0.5)\r\n \"\"\"\r\n settings.type = 'HAIR'\r\n settings.count = count\r\n settings.hair_length = hair_length\r\n settings.hair_step = segments\r\n settings.emit_from = emit_from\r\n settings.use_advanced_hair = use_advanced\r\n\r\n\r\ndef set_particle_physics(\r\n settings: bpy.types.ParticleSettings,\r\n physics_type: str = 'NEWTON',\r\n gravity: float = 1.0,\r\n mass: float = 1.0,\r\n drag: float = 0.0,\r\n brownian: float = 0.0\r\n) -> None:\r\n \"\"\"\r\n Configure particle physics.\r\n \r\n Args:\r\n settings: Particle settings object\r\n physics_type: 'NO', 'NEWTON', 'KEYED', 'BOIDS', 'FLUID'\r\n gravity: Gravity influence (0-1)\r\n mass: Particle mass\r\n drag: Air resistance\r\n brownian: Random motion\r\n \r\n Example:\r\n >>> set_particle_physics(settings, gravity=0.5, drag=0.1)\r\n \"\"\"\r\n settings.physics_type = physics_type\r\n settings.effector_weights.gravity = gravity\r\n settings.mass = mass\r\n settings.drag_factor = drag\r\n settings.brownian_factor = brownian\r\n\r\n\r\ndef render_as_object(\r\n settings: bpy.types.ParticleSettings,\r\n instance_object: bpy.types.Object,\r\n scale: float = 1.0,\r\n scale_random: float = 0.0,\r\n use_rotation: bool = True,\r\n rotation_random: float = 0.0\r\n) -> None:\r\n \"\"\"\r\n Render particles as instances of an object.\r\n \r\n Args:\r\n settings: Particle settings object\r\n instance_object: Object to instance at each particle\r\n scale: Instance scale\r\n scale_random: Random scale variation\r\n use_rotation: Use particle rotation\r\n rotation_random: Random rotation factor\r\n \r\n Example:\r\n >>> render_as_object(settings, leaf_mesh, scale=0.1, scale_random=0.3)\r\n \"\"\"\r\n settings.render_type = 'OBJECT'\r\n settings.instance_object = instance_object\r\n settings.particle_size = scale\r\n settings.size_random = scale_random\r\n settings.use_rotation_instance = use_rotation\r\n settings.rotation_factor_random = rotation_random\r\n\r\n\r\ndef render_as_collection(\r\n settings: bpy.types.ParticleSettings,\r\n collection: bpy.types.Collection,\r\n scale: float = 1.0,\r\n use_whole_collection: bool = True,\r\n use_rotation: bool = True\r\n) -> None:\r\n \"\"\"\r\n Render particles as instances from a collection.\r\n \r\n Args:\r\n settings: Particle settings object\r\n collection: Collection containing instance objects\r\n scale: Instance scale\r\n use_whole_collection: Use all objects (False = pick random)\r\n use_rotation: Use particle rotation\r\n \r\n Example:\r\n >>> render_as_collection(settings, bpy.data.collections['Rocks'])\r\n \"\"\"\r\n settings.render_type = 'COLLECTION'\r\n settings.instance_collection = collection\r\n settings.particle_size = scale\r\n settings.use_whole_collection = use_whole_collection\r\n settings.use_rotation_instance = use_rotation\r\n\r\n\r\ndef add_force_field(\r\n location: tuple = (0, 0, 0),\r\n field_type: str = 'FORCE',\r\n strength: float = 1.0,\r\n falloff_type: str = 'SPHERE',\r\n falloff_power: float = 2.0,\r\n name: str = \"ForceField\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add a force field to affect particles.\r\n \r\n Args:\r\n location: Field position\r\n field_type: 'FORCE', 'WIND', 'VORTEX', 'MAGNETIC', 'HARMONIC',\r\n 'CHARGE', 'LENNARD_JONES', 'TEXTURE', 'GUIDE',\r\n 'BOID', 'TURBULENCE', 'DRAG'\r\n strength: Force strength\r\n falloff_type: 'SPHERE', 'TUBE', 'CONE'\r\n falloff_power: Distance falloff exponent\r\n name: Object name\r\n \r\n Returns:\r\n The created force field object\r\n \r\n Example:\r\n >>> wind = add_force_field((0, -5, 5), 'WIND', strength=10)\r\n \"\"\"\r\n bpy.ops.object.effector_add(type=field_type, location=location)\r\n field = bpy.context.active_object\r\n field.name = name\r\n \r\n field.field.strength = strength\r\n field.field.shape = falloff_type\r\n field.field.falloff_power = falloff_power\r\n \r\n return field\r\n\r\n\r\ndef convert_particles_to_mesh(\r\n obj: bpy.types.Object,\r\n apply_modifiers: bool = True\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Convert particle instances to real mesh objects.\r\n \r\n Args:\r\n obj: Object with particle system\r\n apply_modifiers: Apply all modifiers\r\n \r\n Returns:\r\n The created mesh object\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.select_set(True)\r\n \r\n bpy.ops.object.duplicates_make_real(use_base_parent=True, use_hierarchy=True)\r\n \r\n # The new objects are now selected\r\n return bpy.context.selected_objects\r\n\r\n\r\ndef create_rain_effect(\r\n emitter: bpy.types.Object,\r\n intensity: int = 1000,\r\n speed: float = 5.0,\r\n area_size: float = 10.0,\r\n name: str = \"Rain\"\r\n) -> tuple:\r\n \"\"\"\r\n Create a rain particle effect.\r\n \r\n Args:\r\n emitter: Surface to emit rain from (usually a plane above scene)\r\n intensity: Number of particles\r\n speed: Fall speed\r\n area_size: Emission area size\r\n name: System name\r\n \r\n Returns:\r\n Tuple of (particle_system, settings)\r\n \r\n Example:\r\n >>> rain_plane = bpy.data.objects['RainPlane']\r\n >>> ps, settings = create_rain_effect(rain_plane, intensity=5000)\r\n \"\"\"\r\n ps, settings = add_particle_system(emitter, name, 'EMITTER')\r\n \r\n # Configure as rain\r\n settings.count = intensity\r\n settings.lifetime = 50\r\n settings.frame_start = 1\r\n settings.frame_end = 250\r\n settings.emit_from = 'FACE'\r\n \r\n # Physics\r\n settings.physics_type = 'NEWTON'\r\n settings.normal_factor = 0\r\n settings.object_align_factor = (0, 0, -speed)\r\n settings.effector_weights.gravity = 1.0\r\n \r\n # Appearance\r\n settings.render_type = 'LINE'\r\n settings.line_length_tail = 1.0\r\n \r\n return ps, settings\r\n\r\n\r\ndef create_dust_effect(\r\n emitter: bpy.types.Object,\r\n count: int = 500,\r\n size: float = 0.02,\r\n movement: float = 0.3,\r\n name: str = \"Dust\"\r\n) -> tuple:\r\n \"\"\"\r\n Create floating dust particles.\r\n \r\n Args:\r\n emitter: Volume or surface to emit from\r\n count: Number of particles\r\n size: Particle size\r\n movement: Brownian motion amount\r\n name: System name\r\n \r\n Returns:\r\n Tuple of (particle_system, settings)\r\n \"\"\"\r\n ps, settings = add_particle_system(emitter, name, 'EMITTER')\r\n \r\n settings.count = count\r\n settings.lifetime = 500\r\n settings.frame_start = 1\r\n settings.frame_end = 1\r\n settings.emit_from = 'VOLUME'\r\n \r\n # Physics - floating motion\r\n settings.physics_type = 'NEWTON'\r\n settings.brownian_factor = movement\r\n settings.effector_weights.gravity = 0.0\r\n settings.drag_factor = 1.0\r\n \r\n # Appearance\r\n settings.render_type = 'HALO'\r\n settings.particle_size = size\r\n settings.size_random = 0.5\r\n \r\n return ps, settings","category":"utility","source_file":"particle_utils.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Photorealistic PBR Texture Materials β€” Blender 4.0/5.0. Complete image-based PBR material workflows for photorealistic rendering. Teaches how to set up materials using texture maps (not just procedural). PBR TEXTURE MAP CONNECTIONS:","output":"import bpy\r\nimport os\r\n\r\n\r\ndef create_pbr_material(name, textures_dir,\r\n base_color_file=None,\r\n roughness_file=None,\r\n metallic_file=None,\r\n normal_file=None,\r\n height_file=None,\r\n ao_file=None,\r\n displacement_scale=0.1):\r\n \"\"\"Create a full PBR material from image texture files.\r\n\r\n This function creates a production-ready PBR material by loading\r\n texture map files and connecting them to the Principled BSDF.\r\n\r\n Args:\r\n name: Material name\r\n textures_dir: Directory containing texture files\r\n base_color_file: Filename for albedo/diffuse map (sRGB)\r\n roughness_file: Filename for roughness map (Non-Color)\r\n metallic_file: Filename for metallic map (Non-Color)\r\n normal_file: Filename for normal map (Non-Color)\r\n height_file: Filename for height/bump map (Non-Color)\r\n ao_file: Filename for ambient occlusion map (Non-Color)\r\n displacement_scale: Scale for true displacement\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Core nodes\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (800, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (400, 0)\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n # Texture Coordinate + Mapping for UV control\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-1200, 0)\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-1000, 0)\r\n links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])\r\n\r\n y_offset = 300\r\n x_base = -600\r\n\r\n # --- Base Color (sRGB) ---\r\n if base_color_file:\r\n bc_tex = _add_image_texture(\r\n nodes, links, mapping,\r\n os.path.join(textures_dir, base_color_file),\r\n color_space='sRGB',\r\n location=(x_base, y_offset)\r\n )\r\n # If AO map exists, multiply it with base color\r\n if ao_file:\r\n ao_tex = _add_image_texture(\r\n nodes, links, mapping,\r\n os.path.join(textures_dir, ao_file),\r\n color_space='Non-Color',\r\n location=(x_base, y_offset - 200)\r\n )\r\n mix = nodes.new('ShaderNodeMix')\r\n mix.data_type = 'RGBA'\r\n mix.blend_type = 'MULTIPLY'\r\n mix.location = (x_base + 300, y_offset)\r\n mix.inputs[0].default_value = 1.0 # Factor\r\n links.new(bc_tex.outputs['Color'], mix.inputs[6]) # A\r\n links.new(ao_tex.outputs['Color'], mix.inputs[7]) # B\r\n links.new(mix.outputs[2], bsdf.inputs['Base Color'])\r\n else:\r\n links.new(bc_tex.outputs['Color'], bsdf.inputs['Base Color'])\r\n\r\n # --- Roughness (Non-Color) ---\r\n if roughness_file:\r\n rough_tex = _add_image_texture(\r\n nodes, links, mapping,\r\n os.path.join(textures_dir, roughness_file),\r\n color_space='Non-Color',\r\n location=(x_base, y_offset - 400)\r\n )\r\n links.new(rough_tex.outputs['Color'], bsdf.inputs['Roughness'])\r\n\r\n # --- Metallic (Non-Color) ---\r\n if metallic_file:\r\n metal_tex = _add_image_texture(\r\n nodes, links, mapping,\r\n os.path.join(textures_dir, metallic_file),\r\n color_space='Non-Color',\r\n location=(x_base, y_offset - 600)\r\n )\r\n links.new(metal_tex.outputs['Color'], bsdf.inputs['Metallic'])\r\n\r\n # --- Normal Map (Non-Color, needs Normal Map node) ---\r\n if normal_file:\r\n norm_tex = _add_image_texture(\r\n nodes, links, mapping,\r\n os.path.join(textures_dir, normal_file),\r\n color_space='Non-Color',\r\n location=(x_base, y_offset - 800)\r\n )\r\n normal_node = nodes.new('ShaderNodeNormalMap')\r\n normal_node.location = (x_base + 300, y_offset - 800)\r\n normal_node.inputs['Strength'].default_value = 1.0\r\n links.new(norm_tex.outputs['Color'], normal_node.inputs['Color'])\r\n links.new(normal_node.outputs['Normal'], bsdf.inputs['Normal'])\r\n\r\n # --- Height/Bump (Non-Color, uses Bump node) ---\r\n if height_file and not normal_file:\r\n # Use Bump node only if no Normal Map (they're redundant together)\r\n height_tex = _add_image_texture(\r\n nodes, links, mapping,\r\n os.path.join(textures_dir, height_file),\r\n color_space='Non-Color',\r\n location=(x_base, y_offset - 1000)\r\n )\r\n bump = nodes.new('ShaderNodeBump')\r\n bump.location = (x_base + 300, y_offset - 1000)\r\n bump.inputs['Strength'].default_value = 0.5\r\n links.new(height_tex.outputs['Color'], bump.inputs['Height'])\r\n links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])\r\n\r\n # --- True Displacement (Cycles only) ---\r\n if height_file:\r\n mat.displacement_method = 'BOTH'\r\n height_tex_disp = _add_image_texture(\r\n nodes, links, mapping,\r\n os.path.join(textures_dir, height_file),\r\n color_space='Non-Color',\r\n location=(x_base, y_offset - 1200)\r\n )\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (400, -300)\r\n disp.inputs['Scale'].default_value = displacement_scale\r\n disp.inputs['Midlevel'].default_value = 0.5\r\n links.new(height_tex_disp.outputs['Color'], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat\r\n\r\n\r\ndef _add_image_texture(nodes, links, mapping_node, filepath,\r\n color_space='sRGB', location=(0, 0)):\r\n \"\"\"Helper: create an Image Texture node, load image, connect mapping.\"\"\"\r\n tex = nodes.new('ShaderNodeTexImage')\r\n tex.location = location\r\n tex.image = bpy.data.images.load(filepath)\r\n tex.image.colorspace_settings.name = color_space\r\n links.new(mapping_node.outputs['Vector'], tex.inputs['Vector'])\r\n return tex\r\n\r\n\r\n# =============================================================================\r\n# PROCEDURAL PHOTOREALISTIC MATERIALS (no image files needed)\r\n# =============================================================================\r\n\r\ndef create_procedural_stone_wall(name=\"StoneWall\"):\r\n \"\"\"Create photorealistic stone wall using only procedural textures.\r\n\r\n Uses layered noise + voronoi for stone pattern with mortar gaps.\r\n No image files needed β€” everything is generated procedurally.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.displacement_method = 'BOTH'\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (800, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (400, 0)\r\n bsdf.inputs['Roughness'].default_value = 0.85\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n\r\n # Voronoi for stone cell pattern\r\n voronoi = nodes.new('ShaderNodeTexVoronoi')\r\n voronoi.location = (-400, 200)\r\n voronoi.feature = 'F1'\r\n voronoi.inputs['Scale'].default_value = 4.0\r\n voronoi.inputs['Randomness'].default_value = 0.8\r\n\r\n # Noise for color variation\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, 0)\r\n noise.inputs['Scale'].default_value = 15.0\r\n noise.inputs['Detail'].default_value = 6.0\r\n\r\n # Color ramp for stone vs mortar\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-100, 200)\r\n ramp.color_ramp.elements[0].position = 0.05\r\n ramp.color_ramp.elements[0].color = (0.15, 0.12, 0.10, 1) # Mortar\r\n ramp.color_ramp.elements[1].position = 0.1\r\n ramp.color_ramp.elements[1].color = (0.5, 0.45, 0.38, 1) # Stone\r\n\r\n # Mix stone color with noise variation\r\n mix_color = nodes.new('ShaderNodeMix')\r\n mix_color.location = (100, 100)\r\n mix_color.data_type = 'RGBA'\r\n mix_color.inputs[0].default_value = 0.3\r\n\r\n links.new(tex_coord.outputs['Object'], voronoi.inputs['Vector'])\r\n links.new(tex_coord.outputs['Object'], noise.inputs['Vector'])\r\n links.new(voronoi.outputs['Distance'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], mix_color.inputs[6])\r\n links.new(noise.outputs['Color'], mix_color.inputs[7])\r\n links.new(mix_color.outputs[2], bsdf.inputs['Base Color'])\r\n\r\n # Displacement from voronoi distance\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (400, -200)\r\n disp.inputs['Scale'].default_value = 0.05\r\n links.new(voronoi.outputs['Distance'], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat\r\n\r\n\r\ndef create_procedural_wood_floor(name=\"WoodFloor\"):\r\n \"\"\"Create procedural wood planks material.\r\n\r\n Uses Wave Texture in BANDS mode for wood grain pattern.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (800, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (400, 0)\r\n bsdf.inputs['Roughness'].default_value = 0.55\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n\r\n # Wave for grain lines\r\n wave = nodes.new('ShaderNodeTexWave')\r\n wave.location = (-400, 100)\r\n wave.wave_type = 'BANDS'\r\n wave.wave_profile = 'SAW'\r\n wave.bands_direction = 'Y'\r\n wave.inputs['Scale'].default_value = 2.0\r\n wave.inputs['Distortion'].default_value = 8.0\r\n wave.inputs['Detail'].default_value = 3.0\r\n wave.inputs['Detail Scale'].default_value = 1.5\r\n\r\n # Color ramp for wood tones\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-100, 100)\r\n ramp.color_ramp.elements[0].color = (0.25, 0.15, 0.08, 1) # Dark wood\r\n ramp.color_ramp.elements[1].color = (0.55, 0.35, 0.18, 1) # Light wood\r\n\r\n # Subtle noise for grain variation\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, -100)\r\n noise.inputs['Scale'].default_value = 50.0\r\n noise.inputs['Detail'].default_value = 4.0\r\n\r\n # Bump for surface texture\r\n bump = nodes.new('ShaderNodeBump')\r\n bump.location = (100, -200)\r\n bump.inputs['Strength'].default_value = 0.3\r\n\r\n links.new(tex_coord.outputs['Object'], wave.inputs['Vector'])\r\n links.new(tex_coord.outputs['Object'], noise.inputs['Vector'])\r\n links.new(wave.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n links.new(noise.outputs['Fac'], bump.inputs['Height'])\r\n links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])\r\n\r\n return mat","category":"utility","source_file":"photorealistic_materials.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Create photorealistic stone wall using only procedural textures.\r\n\r\n Uses layered noise + voronoi for stone pattern with mortar gaps.\r\n No image files needed β€” everything is generated procedurally.","output":"import bpy\n\ndef create_procedural_stone_wall(name=\"StoneWall\"):\n \"\"\"Create photorealistic stone wall using only procedural textures.\r\n\r\n Uses layered noise + voronoi for stone pattern with mortar gaps.\r\n No image files needed β€” everything is generated procedurally.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.displacement_method = 'BOTH'\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (800, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (400, 0)\r\n bsdf.inputs['Roughness'].default_value = 0.85\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n\r\n # Voronoi for stone cell pattern\r\n voronoi = nodes.new('ShaderNodeTexVoronoi')\r\n voronoi.location = (-400, 200)\r\n voronoi.feature = 'F1'\r\n voronoi.inputs['Scale'].default_value = 4.0\r\n voronoi.inputs['Randomness'].default_value = 0.8\r\n\r\n # Noise for color variation\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, 0)\r\n noise.inputs['Scale'].default_value = 15.0\r\n noise.inputs['Detail'].default_value = 6.0\r\n\r\n # Color ramp for stone vs mortar\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-100, 200)\r\n ramp.color_ramp.elements[0].position = 0.05\r\n ramp.color_ramp.elements[0].color = (0.15, 0.12, 0.10, 1) # Mortar\r\n ramp.color_ramp.elements[1].position = 0.1\r\n ramp.color_ramp.elements[1].color = (0.5, 0.45, 0.38, 1) # Stone\r\n\r\n # Mix stone color with noise variation\r\n mix_color = nodes.new('ShaderNodeMix')\r\n mix_color.location = (100, 100)\r\n mix_color.data_type = 'RGBA'\r\n mix_color.inputs[0].default_value = 0.3\r\n\r\n links.new(tex_coord.outputs['Object'], voronoi.inputs['Vector'])\r\n links.new(tex_coord.outputs['Object'], noise.inputs['Vector'])\r\n links.new(voronoi.outputs['Distance'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], mix_color.inputs[6])\r\n links.new(noise.outputs['Color'], mix_color.inputs[7])\r\n links.new(mix_color.outputs[2], bsdf.inputs['Base Color'])\r\n\r\n # Displacement from voronoi distance\r\n disp = nodes.new('ShaderNodeDisplacement')\r\n disp.location = (400, -200)\r\n disp.inputs['Scale'].default_value = 0.05\r\n links.new(voronoi.outputs['Distance'], disp.inputs['Height'])\r\n links.new(disp.outputs['Displacement'], output.inputs['Displacement'])\r\n\r\n return mat","category":"utility","source_file":"photorealistic_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create procedural wood planks material.\r\n\r\n Uses Wave Texture in BANDS mode for wood grain pattern.","output":"import bpy\n\ndef create_procedural_wood_floor(name=\"WoodFloor\"):\n \"\"\"Create procedural wood planks material.\r\n\r\n Uses Wave Texture in BANDS mode for wood grain pattern.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (800, 0)\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (400, 0)\r\n bsdf.inputs['Roughness'].default_value = 0.55\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n\r\n # Wave for grain lines\r\n wave = nodes.new('ShaderNodeTexWave')\r\n wave.location = (-400, 100)\r\n wave.wave_type = 'BANDS'\r\n wave.wave_profile = 'SAW'\r\n wave.bands_direction = 'Y'\r\n wave.inputs['Scale'].default_value = 2.0\r\n wave.inputs['Distortion'].default_value = 8.0\r\n wave.inputs['Detail'].default_value = 3.0\r\n wave.inputs['Detail Scale'].default_value = 1.5\r\n\r\n # Color ramp for wood tones\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-100, 100)\r\n ramp.color_ramp.elements[0].color = (0.25, 0.15, 0.08, 1) # Dark wood\r\n ramp.color_ramp.elements[1].color = (0.55, 0.35, 0.18, 1) # Light wood\r\n\r\n # Subtle noise for grain variation\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, -100)\r\n noise.inputs['Scale'].default_value = 50.0\r\n noise.inputs['Detail'].default_value = 4.0\r\n\r\n # Bump for surface texture\r\n bump = nodes.new('ShaderNodeBump')\r\n bump.location = (100, -200)\r\n bump.inputs['Strength'].default_value = 0.3\r\n\r\n links.new(tex_coord.outputs['Object'], wave.inputs['Vector'])\r\n links.new(tex_coord.outputs['Object'], noise.inputs['Vector'])\r\n links.new(wave.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n links.new(noise.outputs['Fac'], bump.inputs['Height'])\r\n links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])\r\n\r\n return mat","category":"utility","source_file":"photorealistic_materials.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Physics Simulation Utilities. Utility functions for setting up physics simulations in Blender.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef add_rigid_body(\r\n obj: bpy.types.Object,\r\n body_type: str = 'ACTIVE',\r\n mass: float = 1.0,\r\n friction: float = 0.5,\r\n bounciness: float = 0.0,\r\n collision_shape: str = 'CONVEX_HULL'\r\n) -> bpy.types.RigidBodyObject:\r\n \"\"\"\r\n Add rigid body physics to an object.\r\n \r\n Args:\r\n obj: Object to add physics to\r\n body_type: 'ACTIVE' (simulated) or 'PASSIVE' (static)\r\n mass: Object mass in kg\r\n friction: Surface friction (0-1)\r\n bounciness: Bounce factor (0-1)\r\n collision_shape: 'BOX', 'SPHERE', 'CAPSULE', 'CYLINDER', 'CONE',\r\n 'CONVEX_HULL', or 'MESH'\r\n \r\n Returns:\r\n The rigid body settings\r\n \r\n Example:\r\n >>> add_rigid_body(cube, 'ACTIVE', mass=2.0, bounciness=0.5)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.rigidbody.object_add(type=body_type)\r\n \r\n rb = obj.rigid_body\r\n rb.mass = mass\r\n rb.friction = friction\r\n rb.restitution = bounciness\r\n rb.collision_shape = collision_shape\r\n \r\n return rb\r\n\r\n\r\ndef create_ground_plane(\r\n size: float = 20.0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Ground\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a ground plane with passive rigid body physics.\r\n \r\n Args:\r\n size: Plane size\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The created ground plane\r\n \r\n Example:\r\n >>> ground = create_ground_plane(size=50)\r\n \"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=location)\r\n ground = bpy.context.active_object\r\n ground.name = name\r\n \r\n # Add passive rigid body\r\n add_rigid_body(ground, 'PASSIVE', collision_shape='BOX')\r\n \r\n # Add simple material\r\n mat = bpy.data.materials.new(name=f\"{name}_Mat\")\r\n mat.node_tree.nodes[\"Principled BSDF\"].inputs['Base Color'].default_value = (0.3, 0.3, 0.3, 1.0)\r\n mat.node_tree.nodes[\"Principled BSDF\"].inputs['Roughness'].default_value = 0.8\r\n ground.data.materials.append(mat)\r\n \r\n return ground\r\n\r\n\r\ndef add_soft_body(\r\n obj: bpy.types.Object,\r\n goal_strength: float = 0.7,\r\n friction: float = 0.5,\r\n mass: float = 1.0,\r\n springs: float = 0.5\r\n) -> bpy.types.SoftBodyModifier:\r\n \"\"\"\r\n Add soft body physics to an object.\r\n \r\n Args:\r\n obj: Object to make soft\r\n goal_strength: How much original shape is maintained (0-1)\r\n friction: Surface friction\r\n mass: Object mass\r\n springs: Spring stiffness (0-1)\r\n \r\n Returns:\r\n The soft body modifier\r\n \r\n Example:\r\n >>> add_soft_body(jelly_cube, goal_strength=0.3, springs=0.8)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.modifier_add(type='SOFT_BODY')\r\n \r\n sb = obj.modifiers[\"Softbody\"].settings\r\n sb.mass = mass\r\n sb.friction = friction\r\n sb.goal_spring = goal_strength\r\n sb.pull = springs\r\n sb.push = springs\r\n \r\n return obj.modifiers[\"Softbody\"]\r\n\r\n\r\ndef add_cloth(\r\n obj: bpy.types.Object,\r\n preset: str = 'COTTON',\r\n quality: int = 5,\r\n collision: bool = True\r\n) -> bpy.types.ClothModifier:\r\n \"\"\"\r\n Add cloth simulation to an object.\r\n \r\n Args:\r\n obj: Mesh object (should be subdivided)\r\n preset: 'COTTON', 'SILK', 'LEATHER', 'RUBBER', 'DENIM'\r\n quality: Simulation quality steps\r\n collision: Enable self-collision\r\n \r\n Returns:\r\n The cloth modifier\r\n \r\n Example:\r\n >>> add_cloth(flag_mesh, preset='SILK', quality=10)\r\n \"\"\"\r\n presets = {\r\n 'COTTON': {'tension': 15, 'compression': 15, 'bending': 0.5, 'mass': 0.3},\r\n 'SILK': {'tension': 5, 'compression': 5, 'bending': 0.1, 'mass': 0.15},\r\n 'LEATHER': {'tension': 80, 'compression': 80, 'bending': 1.5, 'mass': 0.4},\r\n 'RUBBER': {'tension': 25, 'compression': 25, 'bending': 25, 'mass': 0.3},\r\n 'DENIM': {'tension': 40, 'compression': 40, 'bending': 10, 'mass': 0.4},\r\n }\r\n \r\n values = presets.get(preset.upper(), presets['COTTON'])\r\n \r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.modifier_add(type='CLOTH')\r\n \r\n cloth = obj.modifiers[\"Cloth\"]\r\n settings = cloth.settings\r\n \r\n settings.tension_stiffness = values['tension']\r\n settings.compression_stiffness = values['compression']\r\n settings.bending_stiffness = values['bending']\r\n settings.mass = values['mass']\r\n settings.quality = quality\r\n \r\n if collision:\r\n cloth.collision_settings.use_self_collision = True\r\n \r\n return cloth\r\n\r\n\r\ndef add_collision(\r\n obj: bpy.types.Object,\r\n thickness_outer: float = 0.02,\r\n thickness_inner: float = 0.01,\r\n friction: float = 0.5\r\n) -> bpy.types.CollisionModifier:\r\n \"\"\"\r\n Add collision modifier to make object interact with cloth/soft body.\r\n \r\n Args:\r\n obj: Object to add collision to\r\n thickness_outer: Outer collision shell thickness\r\n thickness_inner: Inner collision shell thickness\r\n friction: Collision friction\r\n \r\n Returns:\r\n The collision modifier\r\n \r\n Example:\r\n >>> add_collision(character_body)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.modifier_add(type='COLLISION')\r\n \r\n collision = obj.modifiers[\"Collision\"]\r\n settings = collision.settings\r\n \r\n settings.thickness_outer = thickness_outer\r\n settings.thickness_inner = thickness_inner\r\n settings.friction_factor = friction\r\n \r\n return collision\r\n\r\n\r\ndef create_force_field(\r\n field_type: str = 'FORCE',\r\n location: tuple = (0, 0, 0),\r\n strength: float = 5.0,\r\n falloff: float = 2.0,\r\n name: str = \"ForceField\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a physics force field.\r\n \r\n Args:\r\n field_type: 'FORCE', 'WIND', 'VORTEX', 'MAGNETIC', 'TURBULENCE'\r\n location: Field position\r\n strength: Force strength\r\n falloff: Distance falloff power\r\n name: Object name\r\n \r\n Returns:\r\n The created force field object\r\n \r\n Example:\r\n >>> wind = create_force_field('WIND', strength=10, location=(0, -5, 2))\r\n \"\"\"\r\n field_types = {\r\n 'FORCE': 'FORCE',\r\n 'WIND': 'WIND',\r\n 'VORTEX': 'VORTEX',\r\n 'MAGNETIC': 'MAGNET',\r\n 'TURBULENCE': 'TURBULENCE'\r\n }\r\n \r\n bpy.ops.object.effector_add(type=field_types.get(field_type.upper(), 'FORCE'), location=location)\r\n field = bpy.context.active_object\r\n field.name = name\r\n \r\n field.field.strength = strength\r\n field.field.falloff_power = falloff\r\n \r\n return field\r\n\r\n\r\ndef bake_physics(\r\n frame_start: int = 1,\r\n frame_end: int = 250,\r\n bake_all: bool = True\r\n) -> None:\r\n \"\"\"\r\n Bake all physics simulations in the scene.\r\n \r\n Args:\r\n frame_start: Start frame\r\n frame_end: End frame\r\n bake_all: Bake all physics types\r\n \r\n Example:\r\n >>> bake_physics(1, 100)\r\n \"\"\"\r\n # Set frame range\r\n bpy.context.scene.frame_start = frame_start\r\n bpy.context.scene.frame_end = frame_end\r\n \r\n # Bake rigid bodies\r\n if bake_all:\r\n try:\r\n bpy.ops.rigidbody.bake_to_keyframes(frame_start=frame_start, frame_end=frame_end)\r\n except:\r\n pass\r\n \r\n print(f\"Physics baked from frame {frame_start} to {frame_end}\")","category":"utility","source_file":"physics_utils.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Procedural Shader Node Recipes. Professional procedural material recipes using Blender shader nodes. Covers noise-based textures, voronoi patterns (stone, scales), gradient effects, bump mapping, color ramp techniques, and multi-material mixing. All created via Python scripting without external textures. Inspired by CGWire shader guide and Blender best practices.","output":"import bpy\r\nimport random\r\n\r\n\r\n# =============================================================================\r\n# PROCEDURAL SHADER NODE RECIPES\r\n# =============================================================================\r\n#\r\n# PROCEDURAL MATERIALS = Math, not images.\r\n# Benefits:\r\n# - Infinite resolution (no pixel limit)\r\n# - Fully parametric (change Scale β†’ different look)\r\n# - Animatable (change values over time)\r\n# - No texture files needed\r\n#\r\n# KEY NODES:\r\n# - Noise Texture: organic patterns (wood grain, clouds, dirt)\r\n# - Voronoi Texture: cell patterns (stone, scales, cracks, tiles)\r\n# - Wave Texture: stripes, rings (water ripples, wood rings)\r\n# - Gradient Texture: smooth interpolation (sky, fade effects)\r\n# - Color Ramp: remap any value to colors (THE most useful node)\r\n# - Bump: fake surface detail without geometry\r\n# - Mapping: scale, rotate, offset textures\r\n#\r\n# CONNECTING NODES (Python):\r\n# links = mat.node_tree.links\r\n# links.new(source_node.outputs['Fac'], target_node.inputs['Fac'])\r\n# links.new(source_node.outputs['Color'], target_node.inputs['Base Color'])\r\n# =============================================================================\r\n\r\n\r\ndef create_procedural_noise_material(\r\n name=\"ProceduralNoise\",\r\n color1=(0.02, 0.02, 0.03),\r\n color2=(0.6, 0.35, 0.1),\r\n noise_scale=5.0,\r\n noise_detail=10.0,\r\n roughness=0.4,\r\n metallic=0.0,\r\n bump_strength=0.3\r\n):\r\n \"\"\"\r\n Create a noise-based procedural material with color ramp and bump.\r\n \r\n Great for: organic surfaces, dirt, rust, wood grain, stone variation.\r\n \r\n Args:\r\n name: Material name\r\n color1: Dark color in the ramp\r\n color2: Light color in the ramp\r\n noise_scale: Noise pattern scale (smaller = larger features)\r\n noise_detail: Noise complexity (higher = more detail)\r\n roughness: Surface roughness\r\n metallic: Metallic amount\r\n bump_strength: Bump map intensity (0 = flat, 1 = strong)\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.use_nodes = True\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n \r\n # Principled BSDF\r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (300, 0)\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n bsdf.inputs['Metallic'].default_value = metallic\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n \r\n # Noise Texture\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-500, 0)\r\n noise.inputs['Scale'].default_value = noise_scale\r\n noise.inputs['Detail'].default_value = noise_detail\r\n \r\n # Color Ramp (maps noise to two colors)\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-200, 0)\r\n ramp.color_ramp.elements[0].color = (*color1, 1.0)\r\n ramp.color_ramp.elements[1].color = (*color2, 1.0)\r\n \r\n links.new(noise.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n # Bump map using noise\r\n if bump_strength > 0:\r\n bump = nodes.new('ShaderNodeBump')\r\n bump.location = (100, -200)\r\n bump.inputs['Strength'].default_value = bump_strength\r\n links.new(noise.outputs['Fac'], bump.inputs['Height'])\r\n links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])\r\n \r\n return mat\r\n\r\n\r\ndef create_voronoi_stone_material(\r\n name=\"VoronoiStone\",\r\n color1=(0.3, 0.28, 0.25),\r\n color2=(0.5, 0.48, 0.42),\r\n scale=3.0,\r\n roughness=0.85,\r\n bump_strength=0.5\r\n):\r\n \"\"\"\r\n Create a stone/tile material using Voronoi texture.\r\n \r\n Voronoi naturally creates cell-like patterns perfect for:\r\n stone tiles, dragon scales, cracked earth, cobblestones.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.use_nodes = True\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n \r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n \r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (300, 0)\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n \r\n # Voronoi Texture\r\n voronoi = nodes.new('ShaderNodeTexVoronoi')\r\n voronoi.location = (-500, 0)\r\n voronoi.inputs['Scale'].default_value = scale\r\n voronoi.feature = 'F1' # Closest point distance\r\n \r\n # Color Ramp\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-200, 0)\r\n ramp.color_ramp.elements[0].color = (*color1, 1.0)\r\n ramp.color_ramp.elements[1].color = (*color2, 1.0)\r\n \r\n links.new(voronoi.outputs['Distance'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n # Bump from voronoi for surface detail\r\n bump = nodes.new('ShaderNodeBump')\r\n bump.location = (100, -200)\r\n bump.inputs['Strength'].default_value = bump_strength\r\n links.new(voronoi.outputs['Distance'], bump.inputs['Height'])\r\n links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])\r\n \r\n return mat\r\n\r\n\r\ndef create_gradient_sky_material(\r\n name=\"GradientSky\",\r\n top_color=(0.1, 0.2, 0.6),\r\n bottom_color=(0.8, 0.65, 0.4),\r\n blend_position=0.5\r\n):\r\n \"\"\"\r\n Create a gradient material (useful for sky backgrounds, fade effects).\r\n \r\n Uses Generated texture coordinates with a gradient for smooth blending.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.use_nodes = True\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n \r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n \r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (300, 0)\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n \r\n # Texture Coordinate + Gradient\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-700, 0)\r\n \r\n gradient = nodes.new('ShaderNodeTexGradient')\r\n gradient.location = (-500, 0)\r\n gradient.gradient_type = 'LINEAR'\r\n \r\n # Separate XYZ to get Z (height)\r\n separate = nodes.new('ShaderNodeSeparateXYZ')\r\n separate.location = (-500, -200)\r\n links.new(tex_coord.outputs['Generated'], separate.inputs['Vector'])\r\n \r\n # Color Ramp for gradient colors\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-200, 0)\r\n ramp.color_ramp.elements[0].position = 0.0\r\n ramp.color_ramp.elements[0].color = (*bottom_color, 1.0)\r\n ramp.color_ramp.elements[1].position = blend_position\r\n ramp.color_ramp.elements[1].color = (*top_color, 1.0)\r\n \r\n links.new(separate.outputs['Z'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n return mat\r\n\r\n\r\ndef create_worn_metal_material(\r\n name=\"WornMetal\",\r\n metal_color=(0.6, 0.55, 0.5),\r\n rust_color=(0.35, 0.15, 0.05),\r\n wear_amount=0.4,\r\n base_roughness=0.2,\r\n rust_roughness=0.75\r\n):\r\n \"\"\"\r\n Create a worn/rusted metal material using noise-driven mixing.\r\n \r\n Uses noise texture to blend between clean metal and rusty areas.\r\n Great for: weathered machinery, old robots, medieval armor.\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n mat.use_nodes = True\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n \r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n \r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (300, 0)\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n \r\n # Noise for wear mask\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-600, -200)\r\n noise.inputs['Scale'].default_value = 4.0\r\n noise.inputs['Detail'].default_value = 12.0\r\n noise.inputs['Roughness'].default_value = 0.7\r\n \r\n # Color Ramp to control wear threshold\r\n wear_mask = nodes.new('ShaderNodeValToRGB')\r\n wear_mask.location = (-300, -200)\r\n wear_mask.color_ramp.elements[0].position = wear_amount - 0.1\r\n wear_mask.color_ramp.elements[1].position = wear_amount + 0.1\r\n links.new(noise.outputs['Fac'], wear_mask.inputs['Fac'])\r\n \r\n # Mix Color nodes for base color and roughness\r\n # Use ShaderNodeMix (Blender 4.0+ β€” replaces old MixRGB)\r\n mix_color = nodes.new('ShaderNodeMix')\r\n mix_color.data_type = 'RGBA'\r\n mix_color.location = (0, 100)\r\n mix_color.inputs[6].default_value = (*metal_color, 1.0) # A\r\n mix_color.inputs[7].default_value = (*rust_color, 1.0) # B\r\n links.new(wear_mask.outputs['Color'], mix_color.inputs[0]) # Factor\r\n links.new(mix_color.outputs[2], bsdf.inputs['Base Color']) # Result\r\n \r\n # Mix roughness\r\n mix_rough = nodes.new('ShaderNodeMix')\r\n mix_rough.data_type = 'FLOAT'\r\n mix_rough.location = (0, -100)\r\n mix_rough.inputs[2].default_value = base_roughness # A\r\n mix_rough.inputs[3].default_value = rust_roughness # B\r\n links.new(wear_mask.outputs['Color'], mix_rough.inputs[0])\r\n links.new(mix_rough.outputs[0], bsdf.inputs['Roughness'])\r\n \r\n # Bump\r\n bump = nodes.new('ShaderNodeBump')\r\n bump.location = (150, -300)\r\n bump.inputs['Strength'].default_value = 0.4\r\n links.new(noise.outputs['Fac'], bump.inputs['Height'])\r\n links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])\r\n \r\n return mat\r\n\r\n\r\ndef apply_random_color_variation(obj, base_color=(0.5, 0.5, 0.5), variation=0.1):\r\n \"\"\"\r\n Apply a material with slight random color variation to an object.\r\n \r\n Useful for applying variation across many similar objects (bricks,\r\n leaves, crowd characters) without creating unique textures.\r\n \"\"\"\r\n r = max(0, min(1, base_color[0] + random.uniform(-variation, variation)))\r\n g = max(0, min(1, base_color[1] + random.uniform(-variation, variation)))\r\n b = max(0, min(1, base_color[2] + random.uniform(-variation, variation)))\r\n \r\n mat = bpy.data.materials.new(name=f\"{obj.name}_Material\")\r\n mat.use_nodes = True\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (r, g, b, 1.0)\r\n \r\n obj.data.materials.append(mat)\r\n return mat\r\n\r\n\r\n# =============================================================================\r\n# USAGE EXAMPLES\r\n# =============================================================================\r\n\r\n# --- Example: Worn metal robot ---\r\n# mat = create_worn_metal_material(\r\n# name=\"RobotArmor\",\r\n# metal_color=(0.3, 0.35, 0.4), # Steel blue\r\n# rust_color=(0.4, 0.2, 0.08), # Rust brown\r\n# wear_amount=0.5\r\n# )\r\n# bpy.context.active_object.data.materials.append(mat)\r\n\r\n# --- Example: Alien stone floor ---\r\n# mat = create_voronoi_stone_material(\r\n# name=\"AlienStone\",\r\n# color1=(0.1, 0.15, 0.1),\r\n# color2=(0.2, 0.35, 0.15),\r\n# scale=5.0,\r\n# bump_strength=0.8\r\n# )\r\n\r\n# --- Example: Apply random variations to all selected objects ---\r\n# for obj in bpy.context.selected_objects:\r\n# if obj.type == 'MESH':\r\n# apply_random_color_variation(obj, base_color=(0.8, 0.2, 0.1), variation=0.15)","category":"utility","source_file":"procedural_shader_recipes.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Apply a material with slight random color variation to an object.\r\n \r\n Useful for applying variation across many similar objects (bricks,\r\n leaves, crowd characters) without creating unique textures.","output":"import bpy\n\ndef apply_random_color_variation(obj, base_color=(0.5, 0.5, 0.5), variation=0.1):\n \"\"\"\r\n Apply a material with slight random color variation to an object.\r\n \r\n Useful for applying variation across many similar objects (bricks,\r\n leaves, crowd characters) without creating unique textures.\r\n \"\"\"\r\n r = max(0, min(1, base_color[0] + random.uniform(-variation, variation)))\r\n g = max(0, min(1, base_color[1] + random.uniform(-variation, variation)))\r\n b = max(0, min(1, base_color[2] + random.uniform(-variation, variation)))\r\n \r\n mat = bpy.data.materials.new(name=f\"{obj.name}_Material\")\r\n mat.use_nodes = True\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (r, g, b, 1.0)\r\n \r\n obj.data.materials.append(mat)\r\n return mat","category":"utility","source_file":"procedural_shader_recipes.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Professional PBR Material Recipes β€” Blender 4.0/5.0. Complete recipes for Metallic, Dielectric, Glass, SSS, and Procedural materials using the correct OpenPBR socket names (Blender 4.0+). BLENDER 4.0/5.0 PRINCIPLED BSDF SOCKET REFERENCE:","output":"import bpy\r\n\r\n\r\n# =============================================================================\r\n# CORE SETUP FUNCTION\r\n# =============================================================================\r\n\r\ndef create_pbr_material(name):\r\n \"\"\"Create a clean Principled BSDF material with factory pattern.\r\n Returns (material, principled_node, nodes, links).\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n # use_nodes deprecated in 5.0, always True by default\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new(type='ShaderNodeOutputMaterial')\r\n output.location = (400, 0)\r\n\r\n principled = nodes.new(type='ShaderNodeBsdfPrincipled')\r\n principled.location = (0, 0)\r\n\r\n links.new(principled.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n return mat, principled, nodes, links\r\n\r\n\r\n# =============================================================================\r\n# METALLIC MATERIALS β€” Metallic = 1.0, Base Color = Specular Color\r\n# =============================================================================\r\n# For metals, Base Color represents SPECULAR reflection color (not diffuse).\r\n# Metals have NO diffuse component. Roughness controls polish level.\r\n\r\nMETAL_RECIPES = {\r\n \"Gold\": {\"base_color\": (1.0, 0.766, 0.336, 1.0), \"roughness\": 0.15},\r\n \"Copper\": {\"base_color\": (0.955, 0.637, 0.538, 1.0), \"roughness\": 0.2},\r\n \"Iron\": {\"base_color\": (0.560, 0.570, 0.580, 1.0), \"roughness\": 0.35},\r\n \"Silver\": {\"base_color\": (0.972, 0.960, 0.915, 1.0), \"roughness\": 0.1},\r\n \"Aluminum\": {\"base_color\": (0.913, 0.921, 0.925, 1.0), \"roughness\": 0.25},\r\n \"Titanium\": {\"base_color\": (0.616, 0.582, 0.544, 1.0), \"roughness\": 0.3},\r\n \"Chrome\": {\"base_color\": (0.550, 0.556, 0.554, 1.0), \"roughness\": 0.05},\r\n \"Brass\": {\"base_color\": (0.887, 0.789, 0.434, 1.0), \"roughness\": 0.2},\r\n \"Bronze\": {\"base_color\": (0.804, 0.498, 0.196, 1.0), \"roughness\": 0.3},\r\n}\r\n\r\ndef create_metal(name, base_color, roughness=0.15):\r\n \"\"\"Create a PBR metallic material. Metallic=1.0, colored base.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = base_color\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n return mat\r\n\r\ndef create_gold():\r\n return create_metal(\"Gold\", (1.0, 0.766, 0.336, 1.0), 0.15)\r\n\r\ndef create_copper():\r\n return create_metal(\"Copper\", (0.955, 0.637, 0.538, 1.0), 0.2)\r\n\r\ndef create_chrome():\r\n return create_metal(\"Chrome\", (0.550, 0.556, 0.554, 1.0), 0.05)\r\n\r\n\r\n# =============================================================================\r\n# DIELECTRIC MATERIALS β€” Metallic = 0.0, White Specular, Colored Diffuse\r\n# =============================================================================\r\n# For non-metals, Base Color = diffuse color. Specular reflection is white\r\n# and controlled by IOR (default 1.5 = standard dielectric).\r\n\r\ndef create_plastic(name, color, roughness=0.3):\r\n \"\"\"Create a PBR plastic material. Metallic=0, high-gloss dielectric.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n bsdf.inputs['Specular IOR Level'].default_value = 0.5 # Standard plastic\r\n return mat\r\n\r\ndef create_rubber(name, color):\r\n \"\"\"Matte rubber β€” high roughness, zero specular.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n bsdf.inputs['Specular IOR Level'].default_value = 0.2\r\n return mat\r\n\r\n\r\n# =============================================================================\r\n# GLASS β€” Transmission Weight = 1.0, Low Roughness, IOR = 1.5\r\n# =============================================================================\r\n# In Blender 4.0+: Use \"Transmission Weight\" (was \"Transmission\" in 3.x).\r\n# IOR values: Glass=1.5, Water=1.33, Diamond=2.42, Crystal=2.0.\r\n\r\ndef create_glass(name=\"Glass\", color=(1.0, 1.0, 1.0, 1.0), ior=1.5, roughness=0.0):\r\n \"\"\"Create physically accurate glass material.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Transmission Weight'].default_value = 1.0 # Full transmission\r\n bsdf.inputs['IOR'].default_value = ior\r\n bsdf.inputs['Roughness'].default_value = roughness # 0 = clear, 0.3 = frosted\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n return mat\r\n\r\ndef create_frosted_glass(name=\"FrostedGlass\"):\r\n \"\"\"Frosted glass β€” transmission + medium roughness.\"\"\"\r\n return create_glass(name, roughness=0.3)\r\n\r\ndef create_water(name=\"Water\"):\r\n \"\"\"Water β€” IOR 1.33, slight tint.\"\"\"\r\n return create_glass(name, color=(0.8, 0.9, 1.0, 1.0), ior=1.33)\r\n\r\ndef create_diamond(name=\"Diamond\"):\r\n \"\"\"Diamond β€” IOR 2.42, high dispersion.\"\"\"\r\n return create_glass(name, ior=2.42)\r\n\r\n\r\n# =============================================================================\r\n# SUBSURFACE SCATTERING β€” Subsurface Weight = 1.0, Base Color drives SSS\r\n# =============================================================================\r\n# In Blender 4.0+: \"Subsurface Color\" socket is REMOVED.\r\n# Base Color now drives SSS color directly.\r\n# Use Subsurface Radius to control R/G/B scatter distances.\r\n\r\ndef create_skin(name=\"Skin\"):\r\n \"\"\"Human skin β€” SSS with warm undertone.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.6, 0.5, 1.0)\r\n bsdf.inputs['Subsurface Weight'].default_value = 1.0\r\n bsdf.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1)\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n return mat\r\n\r\ndef create_wax(name=\"Wax\"):\r\n \"\"\"Candle wax β€” strong SSS, warm tint.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.9, 0.7, 1.0)\r\n bsdf.inputs['Subsurface Weight'].default_value = 1.0\r\n bsdf.inputs['Subsurface Radius'].default_value = (0.8, 0.5, 0.2)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n return mat\r\n\r\ndef create_marble(name=\"Marble\"):\r\n \"\"\"Marble β€” SSS with cool blue-white base.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.95, 0.97, 1.0)\r\n bsdf.inputs['Subsurface Weight'].default_value = 0.5\r\n bsdf.inputs['Subsurface Radius'].default_value = (0.6, 0.6, 0.8)\r\n bsdf.inputs['Roughness'].default_value = 0.1\r\n return mat\r\n\r\n\r\n# =============================================================================\r\n# THIN FILM β€” NEW in Blender 4.0 (Soap Bubbles, Oil Slicks)\r\n# =============================================================================\r\n\r\ndef create_soap_bubble(name=\"SoapBubble\"):\r\n \"\"\"Iridescent soap bubble using Thin Film inputs (4.0+).\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.95, 1.0, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n bsdf.inputs['Roughness'].default_value = 0.0\r\n bsdf.inputs['Transmission Weight'].default_value = 0.9\r\n bsdf.inputs['Alpha'].default_value = 0.3\r\n bsdf.inputs['Thin Film Thickness'].default_value = 500.0 # nm\r\n bsdf.inputs['Thin Film IOR'].default_value = 1.4\r\n mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else None\r\n return mat\r\n\r\n\r\n# =============================================================================\r\n# PROCEDURAL GOLD WITH IMPERFECTIONS\r\n# =============================================================================\r\n# Pattern: Macro noise (color variation) + Micro noise (roughness) + Voronoi (bumps)\r\n\r\ndef create_procedural_gold(name=\"Procedural_Gold\"):\r\n \"\"\"Gold with micro-scratches via procedural noise on roughness.\"\"\"\r\n if name in bpy.data.materials:\r\n bpy.data.materials.remove(bpy.data.materials[name])\r\n\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n\r\n # Texture Coordinates β†’ Mapping β†’ Noise\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 200)\r\n\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-600, 200)\r\n\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, 200)\r\n noise.inputs['Scale'].default_value = 50.0\r\n noise.inputs['Detail'].default_value = 15.0\r\n noise.inputs['Roughness'].default_value = 0.6\r\n\r\n # Color Ramp to control roughness range (mostly shiny with variation)\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-200, 200)\r\n ramp.color_ramp.elements[0].position = 0.4\r\n ramp.color_ramp.elements[0].color = (0.05, 0.05, 0.05, 1)\r\n ramp.color_ramp.elements[1].position = 1.0\r\n ramp.color_ramp.elements[1].color = (0.2, 0.2, 0.2, 1)\r\n\r\n # PBR Gold settings\r\n bsdf.inputs['Base Color'].default_value = (1.0, 0.766, 0.336, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n\r\n # Link chain: TexCoord β†’ Mapping β†’ Noise β†’ Ramp β†’ Roughness\r\n links.new(tex_coord.outputs['Object'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], noise.inputs['Vector'])\r\n links.new(noise.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Roughness'])\r\n\r\n return mat\r\n\r\n\r\n# =============================================================================\r\n# EMISSIVE MATERIALS β€” Correct 4.0/5.0 Socket Names\r\n# =============================================================================\r\n\r\ndef create_emissive(name, color, strength=5.0):\r\n \"\"\"Create emissive material. Use 'Emission Color' (was 'Emission' in 3.x).\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Emission Color'].default_value = color # NOT \"Emission\"!\r\n bsdf.inputs['Emission Strength'].default_value = strength\r\n return mat\r\n\r\ndef create_neon_glow(name=\"NeonGlow\", color=(0.0, 1.0, 0.5, 1.0)):\r\n \"\"\"Bright neon glow β€” high emission strength.\"\"\"\r\n return create_emissive(name, color, strength=8.0)","category":"utility","source_file":"professional_materials.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Create a clean Principled BSDF material with factory pattern.\r\n Returns (material, principled_node, nodes, links).","output":"import bpy\n\ndef create_pbr_material(name):\n \"\"\"Create a clean Principled BSDF material with factory pattern.\r\n Returns (material, principled_node, nodes, links).\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n # use_nodes deprecated in 5.0, always True by default\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n output = nodes.new(type='ShaderNodeOutputMaterial')\r\n output.location = (400, 0)\r\n\r\n principled = nodes.new(type='ShaderNodeBsdfPrincipled')\r\n principled.location = (0, 0)\r\n\r\n links.new(principled.outputs['BSDF'], output.inputs['Surface'])\r\n\r\n return mat, principled, nodes, links","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create a PBR metallic material. Metallic=1.0, colored base.","output":"import bpy\n\ndef create_metal(name, base_color, roughness=0.15):\n \"\"\"Create a PBR metallic material. Metallic=1.0, colored base.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = base_color\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Create Gold. Category: utility","output":"import bpy\n\ndef create_gold():\n return create_metal(\"Gold\", (1.0, 0.766, 0.336, 1.0), 0.15)","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Create Copper. Category: utility","output":"import bpy\n\ndef create_copper():\n return create_metal(\"Copper\", (0.955, 0.637, 0.538, 1.0), 0.2)","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Create Chrome. Category: utility","output":"import bpy\n\ndef create_chrome():\n return create_metal(\"Chrome\", (0.550, 0.556, 0.554, 1.0), 0.05)","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create a PBR plastic material. Metallic=0, high-gloss dielectric.","output":"import bpy\n\ndef create_plastic(name, color, roughness=0.3):\n \"\"\"Create a PBR plastic material. Metallic=0, high-gloss dielectric.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n bsdf.inputs['Specular IOR Level'].default_value = 0.5 # Standard plastic\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Matte rubber β€” high roughness, zero specular.","output":"import bpy\n\ndef create_rubber(name, color):\n \"\"\"Matte rubber β€” high roughness, zero specular.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n bsdf.inputs['Specular IOR Level'].default_value = 0.2\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create physically accurate glass material.","output":"import bpy\n\ndef create_glass(name=\"Glass\", color=(1.0, 1.0, 1.0, 1.0), ior=1.5, roughness=0.0):\n \"\"\"Create physically accurate glass material.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Transmission Weight'].default_value = 1.0 # Full transmission\r\n bsdf.inputs['IOR'].default_value = ior\r\n bsdf.inputs['Roughness'].default_value = roughness # 0 = clear, 0.3 = frosted\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Frosted glass β€” transmission + medium roughness.","output":"import bpy\n\ndef create_frosted_glass(name=\"FrostedGlass\"):\n \"\"\"Frosted glass β€” transmission + medium roughness.\"\"\"\r\n return create_glass(name, roughness=0.3)","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Water β€” IOR 1.33, slight tint.","output":"import bpy\n\ndef create_water(name=\"Water\"):\n \"\"\"Water β€” IOR 1.33, slight tint.\"\"\"\r\n return create_glass(name, color=(0.8, 0.9, 1.0, 1.0), ior=1.33)","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Diamond β€” IOR 2.42, high dispersion.","output":"import bpy\n\ndef create_diamond(name=\"Diamond\"):\n \"\"\"Diamond β€” IOR 2.42, high dispersion.\"\"\"\r\n return create_glass(name, ior=2.42)","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Human skin β€” SSS with warm undertone.","output":"import bpy\n\ndef create_skin(name=\"Skin\"):\n \"\"\"Human skin β€” SSS with warm undertone.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.6, 0.5, 1.0)\r\n bsdf.inputs['Subsurface Weight'].default_value = 1.0\r\n bsdf.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1)\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Candle wax β€” strong SSS, warm tint.","output":"import bpy\n\ndef create_wax(name=\"Wax\"):\n \"\"\"Candle wax β€” strong SSS, warm tint.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.9, 0.7, 1.0)\r\n bsdf.inputs['Subsurface Weight'].default_value = 1.0\r\n bsdf.inputs['Subsurface Radius'].default_value = (0.8, 0.5, 0.2)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Marble β€” SSS with cool blue-white base.","output":"import bpy\n\ndef create_marble(name=\"Marble\"):\n \"\"\"Marble β€” SSS with cool blue-white base.\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.95, 0.97, 1.0)\r\n bsdf.inputs['Subsurface Weight'].default_value = 0.5\r\n bsdf.inputs['Subsurface Radius'].default_value = (0.6, 0.6, 0.8)\r\n bsdf.inputs['Roughness'].default_value = 0.1\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Iridescent soap bubble using Thin Film inputs (4.0+).","output":"import bpy\n\ndef create_soap_bubble(name=\"SoapBubble\"):\n \"\"\"Iridescent soap bubble using Thin Film inputs (4.0+).\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.95, 1.0, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n bsdf.inputs['Roughness'].default_value = 0.0\r\n bsdf.inputs['Transmission Weight'].default_value = 0.9\r\n bsdf.inputs['Alpha'].default_value = 0.3\r\n bsdf.inputs['Thin Film Thickness'].default_value = 500.0 # nm\r\n bsdf.inputs['Thin Film IOR'].default_value = 1.4\r\n mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else None\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Gold with micro-scratches via procedural noise on roughness.","output":"import bpy\n\ndef create_procedural_gold(name=\"Procedural_Gold\"):\n \"\"\"Gold with micro-scratches via procedural noise on roughness.\"\"\"\r\n if name in bpy.data.materials:\r\n bpy.data.materials.remove(bpy.data.materials[name])\r\n\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n\r\n # Texture Coordinates β†’ Mapping β†’ Noise\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 200)\r\n\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-600, 200)\r\n\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, 200)\r\n noise.inputs['Scale'].default_value = 50.0\r\n noise.inputs['Detail'].default_value = 15.0\r\n noise.inputs['Roughness'].default_value = 0.6\r\n\r\n # Color Ramp to control roughness range (mostly shiny with variation)\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-200, 200)\r\n ramp.color_ramp.elements[0].position = 0.4\r\n ramp.color_ramp.elements[0].color = (0.05, 0.05, 0.05, 1)\r\n ramp.color_ramp.elements[1].position = 1.0\r\n ramp.color_ramp.elements[1].color = (0.2, 0.2, 0.2, 1)\r\n\r\n # PBR Gold settings\r\n bsdf.inputs['Base Color'].default_value = (1.0, 0.766, 0.336, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n\r\n # Link chain: TexCoord β†’ Mapping β†’ Noise β†’ Ramp β†’ Roughness\r\n links.new(tex_coord.outputs['Object'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], noise.inputs['Vector'])\r\n links.new(noise.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Roughness'])\r\n\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create emissive material. Use 'Emission Color' (was 'Emission' in 3.x).","output":"import bpy\n\ndef create_emissive(name, color, strength=5.0):\n \"\"\"Create emissive material. Use 'Emission Color' (was 'Emission' in 3.x).\"\"\"\r\n mat, bsdf, nodes, links = create_pbr_material(name)\r\n bsdf.inputs['Base Color'].default_value = color\r\n bsdf.inputs['Emission Color'].default_value = color # NOT \"Emission\"!\r\n bsdf.inputs['Emission Strength'].default_value = strength\r\n return mat","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Bright neon glow β€” high emission strength.","output":"import bpy\n\ndef create_neon_glow(name=\"NeonGlow\", color=(0.0, 1.0, 0.5, 1.0)):\n \"\"\"Bright neon glow β€” high emission strength.\"\"\"\r\n return create_emissive(name, color, strength=8.0)","category":"utility","source_file":"professional_materials.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Professional Render Settings β€” Blender 4.0/5.0. Configuration recipes for Cycles and EEVEE, including: - Color management (AgX vs Filmic) - Sample settings for quality vs speed","output":"import bpy\r\n\r\n\r\n# =============================================================================\r\n# CYCLES RENDER SETUP\r\n# =============================================================================\r\n\r\ndef setup_cycles_production(samples=256, preview_samples=64):\r\n \"\"\"Production-quality Cycles render settings with AgX color management.\"\"\"\r\n scene = bpy.context.scene\r\n\r\n # Engine\r\n scene.render.engine = 'CYCLES'\r\n\r\n # Sampling\r\n scene.cycles.samples = samples\r\n scene.cycles.preview_samples = preview_samples\r\n\r\n # Denoising (Cycles)\r\n scene.cycles.use_denoising = True\r\n\r\n # Color Management β€” AgX is the standard in Blender 4.0+\r\n # AgX provides better highlight rolloff than Filmic\r\n scene.view_settings.view_transform = 'AgX'\r\n scene.view_settings.look = 'AgX - High Contrast'\r\n\r\n # Film\r\n scene.render.film_transparent = False\r\n\r\n return scene\r\n\r\ndef setup_cycles_fast(samples=64, preview_samples=16):\r\n \"\"\"Fast preview Cycles settings for iteration.\"\"\"\r\n scene = bpy.context.scene\r\n scene.render.engine = 'CYCLES'\r\n scene.cycles.samples = samples\r\n scene.cycles.preview_samples = preview_samples\r\n scene.view_settings.view_transform = 'AgX'\r\n scene.view_settings.look = 'None'\r\n return scene\r\n\r\n\r\n# =============================================================================\r\n# EEVEE RENDER SETUP β€” Blender 5.0 Compatible\r\n# =============================================================================\r\n# CRITICAL: Engine enum changed in 5.0\r\n# - Pre-5.0: 'BLENDER_EEVEE_NEXT'\r\n# - 5.0+: 'BLENDER_EEVEE'\r\n\r\ndef setup_eevee_production():\r\n \"\"\"Production EEVEE settings for Blender 5.0.\"\"\"\r\n scene = bpy.context.scene\r\n\r\n # Engine β€” use 'BLENDER_EEVEE' (renamed in 5.0)\r\n scene.render.engine = 'BLENDER_EEVEE'\r\n\r\n # EEVEE settings β€” NOTE: some properties moved in 5.0\r\n # scene.eevee.use_gtao = True # REMOVED in 5.0\r\n # scene.eevee.gtao_distance β†’ view_layer.eevee.ambient_occlusion_distance\r\n scene.eevee.use_bloom = True\r\n scene.eevee.use_volumetric_shadows = True\r\n\r\n # Color Management\r\n scene.view_settings.view_transform = 'AgX'\r\n scene.view_settings.look = 'AgX - High Contrast'\r\n\r\n return scene\r\n\r\n\r\n# =============================================================================\r\n# SHADOW CATCHER β€” For Compositing Objects onto Backgrounds\r\n# =============================================================================\r\n\r\ndef setup_shadow_catcher(floor_obj):\r\n \"\"\"Make an object a shadow catcher (Cycles only).\r\n Shadow catchers capture shadows from other objects while being\r\n transparent themselves β€” ideal for product visualization.\r\n \"\"\"\r\n floor_obj.is_shadow_catcher = True\r\n return floor_obj\r\n\r\ndef create_shadow_catcher_plane(size=10):\r\n \"\"\"Create a floor plane as shadow catcher for product shots.\"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=(0, 0, 0))\r\n plane = bpy.context.active_object\r\n plane.name = \"ShadowCatcher_Floor\"\r\n plane.is_shadow_catcher = True\r\n return plane\r\n\r\n\r\n# =============================================================================\r\n# COLOR MANAGEMENT PRESETS\r\n# =============================================================================\r\n# AgX: Better highlight handling, more accurate color reproduction (4.0+ default)\r\n# Filmic: Legacy, good for photorealistic scenes (3.x default)\r\n# Standard: Linear, no tone mapping (for compositing/data passes)\r\n\r\ndef set_color_management_agx(look='AgX - High Contrast'):\r\n \"\"\"Set AgX color management with specified look.\r\n \r\n Valid looks: 'None', 'AgX - Very Low Contrast', 'AgX - Low Contrast',\r\n 'AgX - Medium Low Contrast', 'AgX - Base Contrast', 'AgX - Medium High Contrast',\r\n 'AgX - High Contrast', 'AgX - Very High Contrast'\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.view_settings.view_transform = 'AgX'\r\n scene.view_settings.look = look\r\n scene.display_settings.display_device = 'sRGB'\r\n\r\ndef set_color_management_filmic():\r\n \"\"\"Set Filmic color management (legacy, still useful).\"\"\"\r\n scene = bpy.context.scene\r\n scene.view_settings.view_transform = 'Filmic'\r\n scene.view_settings.look = 'None' # Filmic doesn't use AgX looks\r\n\r\ndef set_color_management_standard():\r\n \"\"\"Set Standard (linear) β€” for compositing and data passes.\"\"\"\r\n scene = bpy.context.scene\r\n scene.view_settings.view_transform = 'Standard'\r\n scene.view_settings.look = 'None'\r\n\r\n\r\n# =============================================================================\r\n# RENDER RESOLUTION PRESETS\r\n# =============================================================================\r\n\r\nRESOLUTION_PRESETS = {\r\n \"1080p\": {\"x\": 1920, \"y\": 1080},\r\n \"720p\": {\"x\": 1280, \"y\": 720},\r\n \"4K\": {\"x\": 3840, \"y\": 2160},\r\n \"Square\": {\"x\": 1080, \"y\": 1080},\r\n \"IG\": {\"x\": 1080, \"y\": 1350}, # Instagram portrait\r\n}\r\n\r\ndef set_render_resolution(preset=\"1080p\", percentage=100):\r\n \"\"\"Set render resolution from preset.\"\"\"\r\n scene = bpy.context.scene\r\n res = RESOLUTION_PRESETS.get(preset, RESOLUTION_PRESETS[\"1080p\"])\r\n scene.render.resolution_x = res[\"x\"]\r\n scene.render.resolution_y = res[\"y\"]\r\n scene.render.resolution_percentage = percentage\r\n\r\n\r\n# =============================================================================\r\n# VIEWPORT MATERIAL PREVIEW\r\n# =============================================================================\r\n\r\ndef set_viewport_material_preview():\r\n \"\"\"Switch 3D viewport to Material Preview mode so materials are visible.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.shading.type = 'MATERIAL'\r\n break\r\n\r\ndef set_viewport_rendered():\r\n \"\"\"Switch 3D viewport to Rendered mode for real-time render preview.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.shading.type = 'RENDERED'\r\n break\r\n\r\n\r\n# =============================================================================\r\n# COMPLETE PRODUCTION SETUP\r\n# =============================================================================\r\n\r\ndef setup_production_scene(\r\n engine='CYCLES',\r\n resolution='1080p',\r\n samples=128,\r\n shadow_catcher=True\r\n):\r\n \"\"\"One-call production setup: engine + resolution + color management.\"\"\"\r\n scene = bpy.context.scene\r\n\r\n if engine == 'CYCLES':\r\n setup_cycles_production(samples=samples)\r\n else:\r\n setup_eevee_production()\r\n\r\n set_render_resolution(resolution)\r\n\r\n if shadow_catcher:\r\n create_shadow_catcher_plane()\r\n\r\n set_viewport_material_preview()\r\n\r\n return scene","category":"utility","source_file":"render_settings.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Production-quality Cycles render settings with AgX color management.","output":"import bpy\n\ndef setup_cycles_production(samples=256, preview_samples=64):\n \"\"\"Production-quality Cycles render settings with AgX color management.\"\"\"\r\n scene = bpy.context.scene\r\n\r\n # Engine\r\n scene.render.engine = 'CYCLES'\r\n\r\n # Sampling\r\n scene.cycles.samples = samples\r\n scene.cycles.preview_samples = preview_samples\r\n\r\n # Denoising (Cycles)\r\n scene.cycles.use_denoising = True\r\n\r\n # Color Management β€” AgX is the standard in Blender 4.0+\r\n # AgX provides better highlight rolloff than Filmic\r\n scene.view_settings.view_transform = 'AgX'\r\n scene.view_settings.look = 'AgX - High Contrast'\r\n\r\n # Film\r\n scene.render.film_transparent = False\r\n\r\n return scene","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Fast preview Cycles settings for iteration.","output":"import bpy\n\ndef setup_cycles_fast(samples=64, preview_samples=16):\n \"\"\"Fast preview Cycles settings for iteration.\"\"\"\r\n scene = bpy.context.scene\r\n scene.render.engine = 'CYCLES'\r\n scene.cycles.samples = samples\r\n scene.cycles.preview_samples = preview_samples\r\n scene.view_settings.view_transform = 'AgX'\r\n scene.view_settings.look = 'None'\r\n return scene","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Production EEVEE settings for Blender 5.0.","output":"import bpy\n\ndef setup_eevee_production():\n \"\"\"Production EEVEE settings for Blender 5.0.\"\"\"\r\n scene = bpy.context.scene\r\n\r\n # Engine β€” use 'BLENDER_EEVEE' (renamed in 5.0)\r\n scene.render.engine = 'BLENDER_EEVEE'\r\n\r\n # EEVEE settings β€” NOTE: some properties moved in 5.0\r\n # scene.eevee.use_gtao = True # REMOVED in 5.0\r\n # scene.eevee.gtao_distance β†’ view_layer.eevee.ambient_occlusion_distance\r\n scene.eevee.use_bloom = True\r\n scene.eevee.use_volumetric_shadows = True\r\n\r\n # Color Management\r\n scene.view_settings.view_transform = 'AgX'\r\n scene.view_settings.look = 'AgX - High Contrast'\r\n\r\n return scene","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Make an object a shadow catcher (Cycles only).\r\n Shadow catchers capture shadows from other objects while being\r\n transparent themselves β€” ideal for product visualization.","output":"import bpy\n\ndef setup_shadow_catcher(floor_obj):\n \"\"\"Make an object a shadow catcher (Cycles only).\r\n Shadow catchers capture shadows from other objects while being\r\n transparent themselves β€” ideal for product visualization.\r\n \"\"\"\r\n floor_obj.is_shadow_catcher = True\r\n return floor_obj","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create a floor plane as shadow catcher for product shots.","output":"import bpy\n\ndef create_shadow_catcher_plane(size=10):\n \"\"\"Create a floor plane as shadow catcher for product shots.\"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=(0, 0, 0))\r\n plane = bpy.context.active_object\r\n plane.name = \"ShadowCatcher_Floor\"\r\n plane.is_shadow_catcher = True\r\n return plane","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Set AgX color management with specified look.\r\n \r\n Valid looks: 'None', 'AgX - Very Low Contrast', 'AgX - Low Contrast',\r\n 'AgX - Medium Low Contrast', 'AgX - Base Contrast', 'AgX - Medium High Contrast',\r\n 'AgX - High Contrast', 'AgX - Very High Contrast'","output":"import bpy\n\ndef set_color_management_agx(look='AgX - High Contrast'):\n \"\"\"Set AgX color management with specified look.\r\n \r\n Valid looks: 'None', 'AgX - Very Low Contrast', 'AgX - Low Contrast',\r\n 'AgX - Medium Low Contrast', 'AgX - Base Contrast', 'AgX - Medium High Contrast',\r\n 'AgX - High Contrast', 'AgX - Very High Contrast'\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.view_settings.view_transform = 'AgX'\r\n scene.view_settings.look = look\r\n scene.display_settings.display_device = 'sRGB'","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Set Filmic color management (legacy, still useful).","output":"import bpy\n\ndef set_color_management_filmic():\n \"\"\"Set Filmic color management (legacy, still useful).\"\"\"\r\n scene = bpy.context.scene\r\n scene.view_settings.view_transform = 'Filmic'\r\n scene.view_settings.look = 'None' # Filmic doesn't use AgX looks","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Set Standard (linear) β€” for compositing and data passes.","output":"import bpy\n\ndef set_color_management_standard():\n \"\"\"Set Standard (linear) β€” for compositing and data passes.\"\"\"\r\n scene = bpy.context.scene\r\n scene.view_settings.view_transform = 'Standard'\r\n scene.view_settings.look = 'None'","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Set render resolution from preset.","output":"import bpy\n\ndef set_render_resolution(preset=\"1080p\", percentage=100):\n \"\"\"Set render resolution from preset.\"\"\"\r\n scene = bpy.context.scene\r\n res = RESOLUTION_PRESETS.get(preset, RESOLUTION_PRESETS[\"1080p\"])\r\n scene.render.resolution_x = res[\"x\"]\r\n scene.render.resolution_y = res[\"y\"]\r\n scene.render.resolution_percentage = percentage","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Switch 3D viewport to Material Preview mode so materials are visible.","output":"import bpy\n\ndef set_viewport_material_preview():\n \"\"\"Switch 3D viewport to Material Preview mode so materials are visible.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.shading.type = 'MATERIAL'\r\n break","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Switch 3D viewport to Rendered mode for real-time render preview.","output":"import bpy\n\ndef set_viewport_rendered():\n \"\"\"Switch 3D viewport to Rendered mode for real-time render preview.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.shading.type = 'RENDERED'\r\n break","category":"utility","source_file":"render_settings.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Render Utilities. Functions for configuring rendering settings, output, and baking.","output":"import bpy\r\nimport os\r\n\r\n\r\ndef render_image(output_path: str = None, open_result: bool = False) -> str:\r\n \"\"\"\r\n Render current frame.\r\n \r\n Args:\r\n output_path: Save path (uses scene path if None)\r\n open_result: Open result in image viewer\r\n \r\n Returns:\r\n Path to rendered image\r\n \r\n Example:\r\n >>> render_image(\"//render_001.png\")\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n if output_path:\r\n scene.render.filepath = output_path\r\n \r\n bpy.ops.render.render(write_still=True)\r\n \r\n if open_result:\r\n bpy.ops.render.view_show()\r\n \r\n return bpy.path.abspath(scene.render.filepath)\r\n\r\n\r\ndef render_animation(\r\n output_path: str = None,\r\n frame_start: int = None,\r\n frame_end: int = None\r\n) -> None:\r\n \"\"\"\r\n Render animation frames.\r\n \r\n Args:\r\n output_path: Output path with frame placeholder (e.g., \"//render_####\")\r\n frame_start: Start frame (uses scene value if None)\r\n frame_end: End frame (uses scene value if None)\r\n \r\n Example:\r\n >>> render_animation(\"//frames/render_####.png\", 1, 120)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n if output_path:\r\n scene.render.filepath = output_path\r\n if frame_start is not None:\r\n scene.frame_start = frame_start\r\n if frame_end is not None:\r\n scene.frame_end = frame_end\r\n \r\n bpy.ops.render.render(animation=True)\r\n\r\n\r\ndef set_render_engine(engine: str = 'CYCLES') -> None:\r\n \"\"\"\r\n Set render engine.\r\n \r\n Args:\r\n engine: 'CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'\r\n \r\n Example:\r\n >>> set_render_engine('CYCLES')\r\n \"\"\"\r\n bpy.context.scene.render.engine = engine\r\n\r\n\r\ndef set_render_samples(\r\n samples: int = 128,\r\n viewport_samples: int = None\r\n) -> None:\r\n \"\"\"\r\n Set render sample count.\r\n \r\n Args:\r\n samples: Final render samples\r\n viewport_samples: Viewport preview samples\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n if scene.render.engine == 'CYCLES':\r\n scene.cycles.samples = samples\r\n if viewport_samples:\r\n scene.cycles.preview_samples = viewport_samples\r\n elif 'EEVEE' in scene.render.engine:\r\n scene.eevee.taa_render_samples = samples\r\n if viewport_samples:\r\n scene.eevee.taa_samples = viewport_samples\r\n\r\n\r\ndef set_output_format(\r\n format: str = 'PNG',\r\n color_mode: str = 'RGBA',\r\n color_depth: str = '8',\r\n compression: int = 15\r\n) -> None:\r\n \"\"\"\r\n Set output image format.\r\n \r\n Args:\r\n format: 'PNG', 'JPEG', 'OPEN_EXR', 'TIFF', 'BMP'\r\n color_mode: 'RGB', 'RGBA', 'BW'\r\n color_depth: '8', '16', '32' (float for EXR)\r\n compression: Compression level (0-100 for PNG)\r\n \r\n Example:\r\n >>> set_output_format('PNG', 'RGBA', '16')\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n scene.render.image_settings.file_format = format\r\n scene.render.image_settings.color_mode = color_mode\r\n \r\n if format == 'PNG':\r\n scene.render.image_settings.color_depth = color_depth\r\n scene.render.image_settings.compression = compression\r\n elif format == 'OPEN_EXR':\r\n scene.render.image_settings.color_depth = color_depth\r\n scene.render.image_settings.exr_codec = 'ZIP'\r\n\r\n\r\ndef set_video_output(\r\n format: str = 'FFMPEG',\r\n codec: str = 'H264',\r\n quality: str = 'HIGH',\r\n audio: bool = True\r\n) -> None:\r\n \"\"\"\r\n Configure video output settings.\r\n \r\n Args:\r\n format: 'FFMPEG', 'AVI_RAW', 'AVI_JPEG'\r\n codec: 'H264', 'MPEG4', 'WEBM', 'PNG'\r\n quality: 'LOWEST', 'LOW', 'MEDIUM', 'HIGH', 'LOSSLESS'\r\n audio: Include audio track\r\n \r\n Example:\r\n >>> set_video_output('FFMPEG', 'H264', 'HIGH')\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n scene.render.image_settings.file_format = format\r\n \r\n if format == 'FFMPEG':\r\n scene.render.ffmpeg.format = 'MPEG4' if codec == 'H264' else codec\r\n scene.render.ffmpeg.codec = codec\r\n scene.render.ffmpeg.constant_rate_factor = quality\r\n \r\n if audio:\r\n scene.render.ffmpeg.audio_codec = 'AAC'\r\n else:\r\n scene.render.ffmpeg.audio_codec = 'NONE'\r\n\r\n\r\ndef bake_textures(\r\n bake_type: str = 'DIFFUSE',\r\n output_size: tuple = (1024, 1024),\r\n margin: int = 16,\r\n use_clear: bool = True,\r\n filepath: str = None\r\n) -> None:\r\n \"\"\"\r\n Bake textures from objects.\r\n \r\n Args:\r\n bake_type: 'DIFFUSE', 'NORMAL', 'AO', 'COMBINED', 'EMIT', 'ROUGHNESS'\r\n output_size: Texture size (width, height)\r\n margin: Edge margin in pixels\r\n use_clear: Clear image before baking\r\n filepath: Output file path\r\n \r\n Example:\r\n >>> bake_textures('NORMAL', (2048, 2048))\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.render.engine = 'CYCLES'\r\n \r\n bpy.ops.object.bake(\r\n type=bake_type,\r\n width=output_size[0],\r\n height=output_size[1],\r\n margin=margin,\r\n use_clear=use_clear,\r\n filepath=filepath if filepath else ''\r\n )\r\n\r\n\r\ndef setup_transparent_background(enable: bool = True) -> None:\r\n \"\"\"\r\n Enable/disable transparent background.\r\n \r\n Args:\r\n enable: True for transparent, False for solid\r\n \"\"\"\r\n bpy.context.scene.render.film_transparent = enable\r\n \r\n if enable:\r\n bpy.context.scene.render.image_settings.color_mode = 'RGBA'\r\n\r\n\r\ndef setup_motion_blur(\r\n enable: bool = True,\r\n shutter: float = 0.5,\r\n position: str = 'CENTER'\r\n) -> None:\r\n \"\"\"\r\n Configure motion blur.\r\n \r\n Args:\r\n enable: Enable motion blur\r\n shutter: Shutter time (0-2, where 1 = full frame)\r\n position: 'START', 'CENTER', 'END'\r\n \r\n Example:\r\n >>> setup_motion_blur(True, shutter=0.3)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n if scene.render.engine == 'CYCLES':\r\n scene.render.use_motion_blur = enable\r\n scene.render.motion_blur_shutter = shutter\r\n scene.render.motion_blur_position = position\r\n elif 'EEVEE' in scene.render.engine:\r\n scene.eevee.use_motion_blur = enable\r\n scene.eevee.motion_blur_shutter = shutter\r\n\r\n\r\ndef add_render_layer_pass(pass_type: str) -> None:\r\n \"\"\"\r\n Enable a render pass for compositing.\r\n \r\n Args:\r\n pass_type: 'diffuse_color', 'specular_color', 'ambient_occlusion',\r\n 'emission', 'environment', 'shadow', 'normal', 'uv', etc.\r\n \"\"\"\r\n view_layer = bpy.context.view_layer\r\n \r\n pass_attrs = {\r\n 'diffuse_color': 'use_pass_diffuse_color',\r\n 'specular_color': 'use_pass_glossy_color',\r\n 'ambient_occlusion': 'use_pass_ambient_occlusion',\r\n 'emission': 'use_pass_emit',\r\n 'environment': 'use_pass_environment',\r\n 'shadow': 'use_pass_shadow',\r\n 'normal': 'use_pass_normal',\r\n 'uv': 'use_pass_uv',\r\n 'z': 'use_pass_z',\r\n 'mist': 'use_pass_mist',\r\n 'object_index': 'use_pass_object_index',\r\n 'material_index': 'use_pass_material_index'\r\n }\r\n \r\n attr = pass_attrs.get(pass_type.lower())\r\n if attr and hasattr(view_layer, attr):\r\n setattr(view_layer, attr, True)\r\n\r\n\r\ndef setup_overscan(percentage: int = 10) -> None:\r\n \"\"\"\r\n Add border around render for edge effects.\r\n \r\n Args:\r\n percentage: Extra render area as percentage\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n # Enable border render\r\n scene.render.use_border = True\r\n scene.render.use_crop_to_border = False\r\n \r\n # Set border with overscan\r\n margin = percentage / 100 / 2\r\n scene.render.border_min_x = -margin\r\n scene.render.border_max_x = 1 + margin\r\n scene.render.border_min_y = -margin\r\n scene.render.border_max_y = 1 + margin","category":"utility","source_file":"render_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Render Image. Category: utility","output":"import bpy\n\ndef render_image(output_path: str = None, open_result: bool = False) -> str:\r\n \"\"\"\r\n Render current frame.\r\n \r\n Args:\r\n output_path: Save path (uses scene path if None)\r\n open_result: Open result in image viewer\r\n \r\n Returns:\r\n Path to rendered image\r\n \r\n Example:\r\n >>> render_image(\"//render_001.png\")\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n if output_path:\r\n scene.render.filepath = output_path\r\n \r\n bpy.ops.render.render(write_still=True)\r\n \r\n if open_result:\r\n bpy.ops.render.view_show()\r\n \r\n return bpy.path.abspath(scene.render.filepath)\r\n\r\n\r\ndef render_animation(\r\n output_path: str = None,\r\n frame_start: int = None,\r\n frame_end: int = None\r\n) -> None:\r\n \"\"\"\r\n Render animation frames.\r\n \r\n Args:\r\n output_path: Output path with frame placeholder (e.g., \"//render_####\")\r\n frame_start: Start frame (uses scene value if None)\r\n frame_end: End frame (uses scene value if None)\r\n \r\n Example:\r\n >>> render_animation(\"//frames/render_####.png\", 1, 120)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n if output_path:\r\n scene.render.filepath = output_path\r\n if frame_start is not None:\r\n scene.frame_start = frame_start\r\n if frame_end is not None:\r\n scene.frame_end = frame_end\r\n \r\n bpy.ops.render.render(animation=True)\r\n\r\n\r\ndef set_render_engine(engine: str = 'CYCLES') -> None:\r\n \"\"\"\r\n Set render engine.\r\n \r\n Args:\r\n engine: 'CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'\r\n \r\n Example:\r\n >>> set_render_engine('CYCLES')\r\n \"\"\"\r\n bpy.context.scene.render.engine = engine\r\n\r\n\r\ndef set_render_samples(\r\n samples: int = 128,\r\n viewport_samples: int = None\r\n) -> None:\r\n \"\"\"\r\n Set render sample count.\r\n \r\n Args:\r\n samples: Final render samples\r\n viewport_samples: Viewport preview samples\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n if scene.render.engine == 'CYCLES':\r\n scene.cycles.samples = samples\r\n if viewport_samples:\r\n scene.cycles.preview_samples = viewport_samples\r\n elif 'EEVEE' in scene.render.engine:\r\n scene.eevee.taa_render_samples = samples\r\n if viewport_samples:\r\n scene.eevee.taa_samples = viewport_samples\r\n\r\n\r\ndef set_output_format(\r\n format: str = 'PNG',\r\n color_mode: str = 'RGBA',\r\n color_depth: str = '8',\r\n compression: int = 15\r\n) -> None:\r\n \"\"\"\r\n Set output image format.\r\n \r\n Args:\r\n format: 'PNG', 'JPEG', 'OPEN_EXR', 'TIFF', 'BMP'\r\n color_mode: 'RGB', 'RGBA', 'BW'\r\n color_depth: '8', '16', '32' (float for EXR)\r\n compression: Compression level (0-100 for PNG)\r\n \r\n Example:\r\n >>> set_output_format('PNG', 'RGBA', '16')\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n scene.render.image_settings.file_format = format\r\n scene.render.image_settings.color_mode = color_mode\r\n \r\n if format == 'PNG':\r\n scene.render.image_settings.color_depth = color_depth\r\n scene.render.image_settings.compression = compression\r\n elif format == 'OPEN_EXR':\r\n scene.render.image_settings.color_depth = color_depth\r\n scene.render.image_settings.exr_codec = 'ZIP'\r\n\r\n\r\ndef set_video_output(\r\n format: str = 'FFMPEG',\r\n codec: str = 'H264',\r\n quality: str = 'HIGH',\r\n audio: bool = True\r\n) -> None:\r\n \"\"\"\r\n Configure video output settings.\r\n \r\n Args:\r\n format: 'FFMPEG', 'AVI_RAW', 'AVI_JPEG'\r\n codec: 'H264', 'MPEG4', 'WEBM', 'PNG'\r\n quality: 'LOWEST', 'LOW', 'MEDIUM', 'HIGH', 'LOSSLESS'\r\n audio: Include audio track\r\n \r\n Example:\r\n >>> set_video_output('FFMPEG', 'H264', 'HIGH')\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n scene.render.image_settings.file_format = format\r\n \r\n if format == 'FFMPEG':\r\n scene.render.ffmpeg.format = 'MPEG4' if codec == 'H264' else codec\r\n scene.render.ffmpeg.codec = codec\r\n scene.render.ffmpeg.constant_rate_factor = quality\r\n \r\n if audio:\r\n scene.render.ffmpeg.audio_codec = 'AAC'\r\n else:\r\n scene.render.ffmpeg.audio_codec = 'NONE'\r\n\r\n\r\ndef bake_textures(\r\n bake_type: str = 'DIFFUSE',\r\n output_size: tuple = (1024, 1024),\r\n margin: int = 16,\r\n use_clear: bool = True,\r\n filepath: str = None\r\n) -> None:\r\n \"\"\"\r\n Bake textures from objects.\r\n \r\n Args:\r\n bake_type: 'DIFFUSE', 'NORMAL', 'AO', 'COMBINED', 'EMIT', 'ROUGHNESS'\r\n output_size: Texture size (width, height)\r\n margin: Edge margin in pixels\r\n use_clear: Clear image before baking\r\n filepath: Output file path\r\n \r\n Example:\r\n >>> bake_textures('NORMAL', (2048, 2048))\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.render.engine = 'CYCLES'\r\n \r\n bpy.ops.object.bake(\r\n type=bake_type,\r\n width=output_size[0],\r\n height=output_size[1],\r\n margin=margin,\r\n use_clear=use_clear,\r\n filepath=filepath if filepath else ''\r\n )\r\n\r\n\r\ndef setup_transparent_background(enable: bool = True) -> None:\r\n \"\"\"\r\n Enable/disable transparent background.\r\n \r\n Args:\r\n enable: True for transparent, False for solid\r\n \"\"\"\r\n bpy.context.scene.render.film_transparent = enable\r\n \r\n if enable:\r\n bpy.context.scene.render.image_settings.color_mode = 'RGBA'\r\n\r\n\r\ndef setup_motion_blur(\r\n enable: bool = True,\r\n shutter: float = 0.5,\r\n position: str = 'CENTER'\r\n) -> None:\r\n \"\"\"\r\n Configure motion blur.\r\n \r\n Args:\r\n enable: Enable motion blur\r\n shutter: Shutter time (0-2, where 1 = full frame)\r\n position: 'START', 'CENTER', 'END'\r\n \r\n Example:\r\n >>> setup_motion_blur(True, shutter=0.3)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n if scene.render.engine == 'CYCLES':\r\n scene.render.use_motion_blur = enable\r\n scene.render.motion_blur_shutter = shutter\r\n scene.render.motion_blur_position = position\r\n elif 'EEVEE' in scene.render.engine:\r\n scene.eevee.use_motion_blur = enable\r\n scene.eevee.motion_blur_shutter = shutter\r\n\r\n\r\ndef add_render_layer_pass(pass_type: str) -> None:\r\n \"\"\"\r\n Enable a render pass for compositing.\r\n \r\n Args:\r\n pass_type: 'diffuse_color', 'specular_color', 'ambient_occlusion',\r\n 'emission', 'environment', 'shadow', 'normal', 'uv', etc.\r\n \"\"\"\r\n view_layer = bpy.context.view_layer\r\n \r\n pass_attrs = {\r\n 'diffuse_color': 'use_pass_diffuse_color',\r\n 'specular_color': 'use_pass_glossy_color',\r\n 'ambient_occlusion': 'use_pass_ambient_occlusion',\r\n 'emission': 'use_pass_emit',\r\n 'environment': 'use_pass_environment',\r\n 'shadow': 'use_pass_shadow',\r\n 'normal': 'use_pass_normal',\r\n 'uv': 'use_pass_uv',\r\n 'z': 'use_pass_z',\r\n 'mist': 'use_pass_mist',\r\n 'object_index': 'use_pass_object_index',\r\n 'material_index': 'use_pass_material_index'\r\n }\r\n \r\n attr = pass_attrs.get(pass_type.lower())\r\n if attr and hasattr(view_layer, attr):\n setattr(view_layer, attr, True)","category":"utility","source_file":"render_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Setup Overscan. Category: utility","output":"import bpy\n\ndef setup_overscan(percentage: int = 10) -> None:\r\n \"\"\"\r\n Add border around render for edge effects.\r\n \r\n Args:\r\n percentage: Extra render area as percentage\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n # Enable border render\r\n scene.render.use_border = True\r\n scene.render.use_crop_to_border = False\r\n \r\n # Set border with overscan\r\n margin = percentage / 100 / 2\r\n scene.render.border_min_x = -margin\r\n scene.render.border_max_x = 1 + margin\r\n scene.render.border_min_y = -margin\r\n scene.render.border_max_y = 1 + margin","category":"utility","source_file":"render_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Scene Composition and Professional Layout. Professional scene composition techniques for Blender. Covers real-world scale references, object grounding (floor planes), backdrop creation, pedestal/display stands, object arrangement patterns (circular, grid, shelf), and skybox setup. Essential for creating polished, realistic scenes that look professional.","output":"import bpy\r\nimport math\r\n\r\n\r\n# =============================================================================\r\n# PROFESSIONAL SCENE COMPOSITION\r\n# =============================================================================\r\n#\r\n# KEY PRINCIPLES FOR PROFESSIONAL SCENES:\r\n#\r\n# 1. GROUNDING: Objects MUST sit on a surface (floor, pedestal, ground plane).\r\n# Floating objects look amateurish. Always add a floor or ground plane.\r\n#\r\n# 2. SCALE: Use real-world dimensions. Blender units = meters by default.\r\n# - Human height: 1.75m\r\n# - Table height: 0.75m\r\n# - Chair height: 0.45m (seat)\r\n# - Door height: 2.0m\r\n# - Room height: 2.5m\r\n# - Car length: 4.5m\r\n#\r\n# 3. COMPOSITION: Don't center everything. Use asymmetry and negative space.\r\n# - Place the main subject slightly off-center\r\n# - Group objects in odd numbers (1, 3, 5)\r\n# - Create depth with foreground, midground, background elements\r\n#\r\n# 4. CONTEXT: Add environmental details (floor, walls, lighting, props)\r\n# to make scenes feel lived-in and realistic.\r\n#\r\n# 5. SPACING: Leave breathing room between objects. Crowded scenes feel chaotic.\r\n# =============================================================================\r\n\r\n\r\n# --- REAL-WORLD SCALE REFERENCE ---\r\nSCALE_REFERENCE = {\r\n # Architecture\r\n 'door_width': 0.9, # meters\r\n 'door_height': 2.1,\r\n 'room_height': 2.5,\r\n 'wall_thickness': 0.15,\r\n 'window_sill': 0.9, # height from floor\r\n 'stair_rise': 0.18, # per step\r\n 'stair_run': 0.28, # per step depth\r\n \r\n # Furniture\r\n 'table_height': 0.75,\r\n 'desk_height': 0.73,\r\n 'desk_width': 1.4,\r\n 'desk_depth': 0.7,\r\n 'chair_seat': 0.45,\r\n 'chair_back': 0.9,\r\n 'sofa_height': 0.4,\r\n 'sofa_depth': 0.9,\r\n 'bed_height': 0.5,\r\n 'bookshelf_h': 1.8,\r\n \r\n # Human\r\n 'human_height': 1.75,\r\n 'eye_level': 1.6,\r\n 'shoulder_w': 0.45,\r\n \r\n # Objects\r\n 'mug_height': 0.1,\r\n 'monitor_h': 0.35,\r\n 'keyboard_w': 0.44,\r\n 'lamp_height': 0.5,\r\n 'book_thickness': 0.03,\r\n \r\n # Vehicles\r\n 'car_length': 4.5,\r\n 'car_width': 1.8,\r\n 'car_height': 1.45,\r\n 'wheel_radius': 0.35,\r\n}\r\n\r\n\r\ndef create_floor_plane(\r\n size=20.0,\r\n location=(0, 0, 0),\r\n color=(0.3, 0.3, 0.32),\r\n roughness=0.8,\r\n name=\"Floor\"\r\n):\r\n \"\"\"\r\n Create a ground plane for objects to sit on.\r\n \r\n ALWAYS add a floor plane unless the scene explicitly doesn't need one\r\n (e.g., space scenes). Objects floating in void look unprofessional.\r\n \r\n Args:\r\n size: Plane size in meters \r\n location: Floor position (default at origin)\r\n color: Floor color (dark gray default)\r\n roughness: Surface roughness (0.8 = matte, 0.1 = glossy)\r\n \"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=location)\r\n floor = bpy.context.active_object\r\n floor.name = name\r\n \r\n # Apply material\r\n mat = bpy.data.materials.new(name=f\"{name}_Material\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n floor.data.materials.append(mat)\r\n \r\n return floor\r\n\r\n\r\ndef create_backdrop(\r\n width=15.0,\r\n height=8.0,\r\n curve_radius=3.0,\r\n color=(0.85, 0.85, 0.87),\r\n name=\"Backdrop\"\r\n):\r\n \"\"\"\r\n Create a seamless backdrop (cyclorama) for studio-style renders.\r\n \r\n Creates a curved floor-to-wall transition that eliminates the\r\n visible seam between floor and background. Professional product\r\n photography standard.\r\n \r\n Args:\r\n width: Backdrop width\r\n height: Wall height\r\n curve_radius: Radius of the floor-to-wall curve\r\n color: Backdrop color (light gray default for studio)\r\n \"\"\"\r\n # Create with a bezier curve profile + extrude\r\n verts = []\r\n segments = 12\r\n \r\n # Floor section\r\n verts.append((-width/2, -5, 0))\r\n verts.append((-width/2, 0, 0))\r\n \r\n # Curved transition\r\n for i in range(segments + 1):\r\n angle = (math.pi / 2) * (i / segments)\r\n y = -curve_radius * math.cos(angle)\r\n z = curve_radius * math.sin(angle)\r\n verts.append((-width/2, y, z))\r\n \r\n # Wall section\r\n verts.append((-width/2, -curve_radius, curve_radius + height))\r\n \r\n # Create mesh\r\n mesh = bpy.data.meshes.new(name)\r\n \r\n # Use simple plane approach for reliability\r\n bpy.ops.mesh.primitive_plane_add(size=width, location=(0, -2, 0))\r\n backdrop = bpy.context.active_object\r\n backdrop.name = name\r\n backdrop.scale = (1, 1.5, 1)\r\n \r\n # Apply material\r\n mat = bpy.data.materials.new(name=f\"{name}_Material\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n backdrop.data.materials.append(mat)\r\n \r\n return backdrop\r\n\r\n\r\ndef create_pedestal(\r\n location=(0, 0, 0),\r\n radius=0.5,\r\n height=0.8,\r\n color=(0.15, 0.15, 0.17),\r\n name=\"Pedestal\"\r\n):\r\n \"\"\"\r\n Create a display pedestal/plinth for showcasing objects.\r\n \r\n Professional product and museum display standard.\r\n Objects should be placed on TOP of the pedestal (at Z = height).\r\n \r\n Args:\r\n location: Base position\r\n radius: Pedestal radius\r\n height: Pedestal height\r\n color: Surface color (dark for contrast)\r\n \"\"\"\r\n x, y, z = location\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius, \r\n depth=height, \r\n location=(x, y, z + height/2)\r\n )\r\n pedestal = bpy.context.active_object\r\n pedestal.name = name\r\n bpy.ops.object.shade_smooth()\r\n \r\n mat = bpy.data.materials.new(name=f\"{name}_Material\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.3\r\n bsdf.inputs['Metallic'].default_value = 0.1\r\n pedestal.data.materials.append(mat)\r\n \r\n return pedestal\r\n\r\n\r\ndef arrange_circular(\r\n objects_data,\r\n center=(0, 0, 0),\r\n radius=5.0,\r\n start_angle=0\r\n):\r\n \"\"\"\r\n Arrange objects in a circular pattern around a center point.\r\n \r\n Args:\r\n objects_data: List of (name, create_func) tuples\r\n center: Center of the circle\r\n radius: Circle radius\r\n start_angle: Starting angle in degrees\r\n \r\n Example:\r\n # Arrange 6 columns around a center point\r\n for i in range(6):\r\n angle = math.radians(i * 60)\r\n x = radius * math.cos(angle)\r\n y = radius * math.sin(angle)\r\n bpy.ops.mesh.primitive_cylinder_add(location=(x, y, 0))\r\n \"\"\"\r\n cx, cy, cz = center\r\n n = len(objects_data)\r\n positions = []\r\n \r\n for i in range(n):\r\n angle = math.radians(start_angle + (360 / n) * i)\r\n x = cx + radius * math.cos(angle)\r\n y = cy + radius * math.sin(angle)\r\n positions.append((x, y, cz))\r\n \r\n return positions\r\n\r\n\r\ndef arrange_grid(\r\n rows=3,\r\n cols=3,\r\n spacing=2.0,\r\n center=(0, 0, 0),\r\n z_offset=0\r\n):\r\n \"\"\"\r\n Calculate grid positions for arranging objects.\r\n \r\n Args:\r\n rows: Number of rows\r\n cols: Number of columns\r\n spacing: Distance between objects\r\n center: Grid center point\r\n z_offset: Height offset for all positions\r\n \r\n Returns:\r\n List of (x, y, z) positions\r\n \"\"\"\r\n cx, cy, cz = center\r\n positions = []\r\n \r\n for row in range(rows):\r\n for col in range(cols):\r\n x = cx + (col - (cols - 1) / 2) * spacing\r\n y = cy + (row - (rows - 1) / 2) * spacing\r\n positions.append((x, y, cz + z_offset))\r\n \r\n return positions\r\n\r\n\r\n# =============================================================================\r\n# COMPLETE SCENE SETUP EXAMPLES\r\n# =============================================================================\r\n\r\n# --- Example: Product showcase (object on pedestal + studio setup) ---\r\n#\r\n# import bpy\r\n#\r\n# # Floor\r\n# create_floor_plane(size=20, color=(0.1, 0.1, 0.12))\r\n#\r\n# # Pedestal\r\n# pedestal = create_pedestal(location=(0, 0, 0), radius=0.4, height=0.6)\r\n#\r\n# # Place product on top of pedestal\r\n# bpy.ops.mesh.primitive_uv_sphere_add(radius=0.3, location=(0, 0, 0.6 + 0.3))\r\n# product = bpy.context.active_object\r\n# product.name = \"Product\"\r\n# bpy.ops.object.shade_smooth()\r\n#\r\n# # Add gold material\r\n# mat = bpy.data.materials.new(\"Gold\")\r\n# bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n# bsdf.inputs['Base Color'].default_value = (1.0, 0.84, 0.0, 1.0)\r\n# bsdf.inputs['Metallic'].default_value = 1.0\r\n# bsdf.inputs['Roughness'].default_value = 0.2\r\n# product.data.materials.append(mat)\r\n\r\n# --- Example: Room interior with furniture ---\r\n#\r\n# # Floor\r\n# create_floor_plane(size=8, color=(0.55, 0.38, 0.18), roughness=0.55, name=\"WoodFloor\")\r\n#\r\n# # Objects at real-world scale\r\n# # Table at 0.75m height\r\n# bpy.ops.mesh.primitive_cube_add(scale=(0.6, 0.4, 0.375), location=(0, 0, 0.375))\r\n# # Chair at 0.45m seat height \r\n# bpy.ops.mesh.primitive_cube_add(scale=(0.2, 0.2, 0.225), location=(0.8, 0, 0.225))","category":"utility","source_file":"scene_composition.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Lighting and Camera Setup. Standard setup for lights (Point, Sun, Area) and cameras in a Blender scene.","output":"import bpy\r\n\r\ndef add_light(type='POINT', location=(5, -5, 5), energy=1000, name=\"Light\"):\r\n bpy.ops.object.light_add(type=type, location=location)\r\n light = bpy.context.active_object\r\n light.name = name\r\n light.data.energy = energy\r\n return light\r\n\r\ndef add_camera(location=(7, -7, 5), rotation=(1.109, 0, 0.814), name=\"Camera\"):\r\n bpy.ops.object.camera_add(location=location, rotation=rotation)\r\n cam = bpy.context.active_object\r\n cam.name = name\r\n bpy.context.scene.camera = cam\r\n return cam\r\n\r\n# Common Three-Point Lighting Setup\r\ndef setup_three_point_lighting():\r\n add_light('AREA', location=(5, -5, 5), energy=1000, name=\"KeyLight\")\r\n add_light('AREA', location=(-5, -5, 3), energy=500, name=\"FillLight\")\r\n add_light('POINT', location=(0, 5, 4), energy=300, name=\"BackLight\")\r\n\r\ndef set_background_color(color=(0.05, 0.05, 0.05, 1.0)):\r\n bg = bpy.context.scene.world.node_tree.nodes['Background']\r\n bg.inputs[0].default_value = color\r\n\r\ndef set_render_engine(engine='CYCLES', samples=128):\r\n bpy.context.scene.render.engine = engine\r\n if engine == 'CYCLES':\r\n bpy.context.scene.cycles.samples = samples\r\n elif engine == 'BLENDER_EEVEE':\r\n bpy.context.scene.eevee.taa_render_samples = samples\r\n\r\ndef set_render_resolution(x=1920, y=1080, percentage=100):\r\n bpy.context.scene.render.resolution_x = x\r\n bpy.context.scene.render.resolution_y = y\r\n bpy.context.scene.render.resolution_percentage = percentage","category":"utility","source_file":"scene_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Add Light. Category: utility","output":"import bpy\n\ndef add_light(type='POINT', location=(5, -5, 5), energy=1000, name=\"Light\"):\n bpy.ops.object.light_add(type=type, location=location)\r\n light = bpy.context.active_object\r\n light.name = name\r\n light.data.energy = energy\r\n return light","category":"utility","source_file":"scene_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Add Camera. Category: utility","output":"import bpy\n\ndef add_camera(location=(7, -7, 5), rotation=(1.109, 0, 0.814), name=\"Camera\"):\n bpy.ops.object.camera_add(location=location, rotation=rotation)\r\n cam = bpy.context.active_object\r\n cam.name = name\r\n bpy.context.scene.camera = cam\r\n return cam","category":"utility","source_file":"scene_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Setup Three Point Lighting. Category: utility","output":"import bpy\n\ndef setup_three_point_lighting():\n add_light('AREA', location=(5, -5, 5), energy=1000, name=\"KeyLight\")\r\n add_light('AREA', location=(-5, -5, 3), energy=500, name=\"FillLight\")\r\n add_light('POINT', location=(0, 5, 4), energy=300, name=\"BackLight\")","category":"utility","source_file":"scene_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Set Background Color. Category: utility","output":"import bpy\n\ndef set_background_color(color=(0.05, 0.05, 0.05, 1.0)):\n bg = bpy.context.scene.world.node_tree.nodes['Background']\r\n bg.inputs[0].default_value = color","category":"utility","source_file":"scene_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Set Render Engine. Category: utility","output":"import bpy\n\ndef set_render_engine(engine='CYCLES', samples=128):\n bpy.context.scene.render.engine = engine\r\n if engine == 'CYCLES':\r\n bpy.context.scene.cycles.samples = samples\r\n elif engine == 'BLENDER_EEVEE':\r\n bpy.context.scene.eevee.taa_render_samples = samples","category":"utility","source_file":"scene_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Set Render Resolution. Category: utility","output":"import bpy\n\ndef set_render_resolution(x=1920, y=1080, percentage=100):\n bpy.context.scene.render.resolution_x = x\r\n bpy.context.scene.render.resolution_y = y\r\n bpy.context.scene.render.resolution_percentage = percentage","category":"utility","source_file":"scene_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Sculpt Utilities. Functions for sculpting mode setup and operations.","output":"import bpy\r\n\r\n\r\ndef enable_sculpt_mode(obj: bpy.types.Object = None) -> None:\r\n \"\"\"Enter sculpt mode for object.\"\"\"\r\n if obj:\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='SCULPT')\r\n\r\n\r\ndef enable_dyntopo(\r\n detail_size: float = 12,\r\n detail_type: str = 'RELATIVE',\r\n refine_method: str = 'SUBDIVIDE_COLLAPSE'\r\n) -> None:\r\n \"\"\"\r\n Enable dynamic topology sculpting.\r\n \r\n Args:\r\n detail_size: Detail level (lower = more detail)\r\n detail_type: 'RELATIVE', 'CONSTANT', 'BRUSH'\r\n refine_method: 'SUBDIVIDE', 'COLLAPSE', 'SUBDIVIDE_COLLAPSE'\r\n \"\"\"\r\n if not bpy.context.sculpt_object.use_dynamic_topology_sculpting:\r\n bpy.ops.sculpt.dynamic_topology_toggle()\r\n \r\n ts = bpy.context.tool_settings.sculpt\r\n ts.detail_size = detail_size\r\n ts.detail_type_method = detail_type\r\n ts.detail_refine_method = refine_method\r\n\r\n\r\ndef disable_dyntopo() -> None:\r\n \"\"\"Disable dynamic topology.\"\"\"\r\n if bpy.context.sculpt_object.use_dynamic_topology_sculpting:\r\n bpy.ops.sculpt.dynamic_topology_toggle()\r\n\r\n\r\ndef add_multires_modifier(\r\n obj: bpy.types.Object,\r\n levels: int = 3\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Add multiresolution modifier for sculpting.\r\n \r\n Args:\r\n obj: Target object\r\n levels: Number of subdivision levels\r\n \r\n Returns:\r\n The multires modifier\r\n \"\"\"\r\n mod = obj.modifiers.new(\"Multires\", 'MULTIRES')\r\n \r\n bpy.context.view_layer.objects.active = obj\r\n for _ in range(levels):\r\n bpy.ops.object.multires_subdivide(modifier=\"Multires\")\r\n \r\n return mod\r\n\r\n\r\ndef remesh_voxel(\r\n obj: bpy.types.Object,\r\n voxel_size: float = 0.01,\r\n smooth_normals: bool = True\r\n) -> None:\r\n \"\"\"\r\n Apply voxel remesh to object.\r\n \r\n Args:\r\n obj: Target object\r\n voxel_size: Voxel size (smaller = more detail)\r\n smooth_normals: Smooth resulting normals\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n obj.data.remesh_voxel_size = voxel_size\r\n obj.data.use_remesh_preserve_volume = True\r\n \r\n bpy.ops.object.voxel_remesh()\r\n \r\n if smooth_normals:\r\n bpy.ops.object.shade_smooth()\r\n\r\n\r\ndef remesh_quadriflow(\r\n obj: bpy.types.Object,\r\n target_faces: int = 5000,\r\n seed: int = 0\r\n) -> None:\r\n \"\"\"\r\n Apply QuadriFlow remesh for clean quad topology.\r\n \r\n Args:\r\n obj: Target object\r\n target_faces: Target face count\r\n seed: Random seed\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.quadriflow_remesh(\r\n target_faces=target_faces,\r\n seed=seed\r\n )\r\n\r\n\r\ndef set_sculpt_brush(brush_name: str) -> None:\r\n \"\"\"\r\n Set active sculpt brush.\r\n \r\n Args:\r\n brush_name: 'Draw', 'Clay', 'Clay Strips', 'Grab', 'Smooth',\r\n 'Flatten', 'Crease', 'Inflate', 'Pinch', 'Mask'\r\n \"\"\"\r\n brush = bpy.data.brushes.get(brush_name)\r\n if brush:\r\n bpy.context.tool_settings.sculpt.brush = brush\r\n\r\n\r\ndef configure_brush(\r\n strength: float = 0.5,\r\n radius: int = 50,\r\n auto_smooth: float = 0,\r\n use_frontface: bool = True\r\n) -> None:\r\n \"\"\"Configure active sculpt brush settings.\"\"\"\r\n brush = bpy.context.tool_settings.sculpt.brush\r\n if brush:\r\n brush.strength = strength\r\n brush.size = radius\r\n brush.auto_smooth_factor = auto_smooth\r\n brush.use_frontface = use_frontface\r\n\r\n\r\ndef mask_by_normal(\r\n limit: float = 0.5,\r\n direction: str = 'VIEW'\r\n) -> None:\r\n \"\"\"Create mask based on normal direction.\"\"\"\r\n bpy.ops.paint.mask_flood_fill(mode='VALUE', value=0)\r\n bpy.ops.sculpt.mask_by_normal(limit=limit)\r\n\r\n\r\ndef expand_mask(iterations: int = 1) -> None:\r\n \"\"\"Expand current mask.\"\"\"\r\n for _ in range(iterations):\r\n bpy.ops.sculpt.mask_expand(\r\n use_smooth=True,\r\n smooth_iterations=2\r\n )\r\n\r\n\r\ndef invert_mask() -> None:\r\n \"\"\"Invert mask.\"\"\"\r\n bpy.ops.paint.mask_flood_fill(mode='INVERT')\r\n\r\n\r\ndef clear_mask() -> None:\r\n \"\"\"Clear all masking.\"\"\"\r\n bpy.ops.paint.mask_flood_fill(mode='VALUE', value=0)\r\n\r\n\r\ndef hide_masked() -> None:\r\n \"\"\"Hide masked geometry.\"\"\"\r\n bpy.ops.sculpt.face_sets_create(mode='MASKED')\r\n bpy.ops.sculpt.face_set_change_visibility(mode='HIDE_ACTIVE')\r\n\r\n\r\ndef show_all() -> None:\r\n \"\"\"Show all hidden geometry.\"\"\"\r\n bpy.ops.sculpt.reveal_all()\r\n\r\n\r\ndef symmetrize_mesh(direction: str = 'NEGATIVE_X') -> None:\r\n \"\"\"\r\n Symmetrize mesh.\r\n \r\n Args:\r\n direction: 'NEGATIVE_X', 'POSITIVE_X', 'NEGATIVE_Y', etc.\r\n \"\"\"\r\n bpy.ops.sculpt.symmetrize(direction=direction)","category":"utility","source_file":"sculpt_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Enable Sculpt Mode. Category: utility","output":"import bpy\n\ndef enable_sculpt_mode(obj: bpy.types.Object = None) -> None:\r\n \"\"\"Enter sculpt mode for object.\"\"\"\r\n if obj:\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='SCULPT')\r\n\r\n\r\ndef enable_dyntopo(\r\n detail_size: float = 12,\r\n detail_type: str = 'RELATIVE',\r\n refine_method: str = 'SUBDIVIDE_COLLAPSE'\r\n) -> None:\r\n \"\"\"\r\n Enable dynamic topology sculpting.\r\n \r\n Args:\r\n detail_size: Detail level (lower = more detail)\r\n detail_type: 'RELATIVE', 'CONSTANT', 'BRUSH'\r\n refine_method: 'SUBDIVIDE', 'COLLAPSE', 'SUBDIVIDE_COLLAPSE'\r\n \"\"\"\r\n if not bpy.context.sculpt_object.use_dynamic_topology_sculpting:\r\n bpy.ops.sculpt.dynamic_topology_toggle()\r\n \r\n ts = bpy.context.tool_settings.sculpt\r\n ts.detail_size = detail_size\r\n ts.detail_type_method = detail_type\r\n ts.detail_refine_method = refine_method\r\n\r\n\r\ndef disable_dyntopo() -> None:\r\n \"\"\"Disable dynamic topology.\"\"\"\r\n if bpy.context.sculpt_object.use_dynamic_topology_sculpting:\r\n bpy.ops.sculpt.dynamic_topology_toggle()\r\n\r\n\r\ndef add_multires_modifier(\r\n obj: bpy.types.Object,\r\n levels: int = 3\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Add multiresolution modifier for sculpting.\r\n \r\n Args:\r\n obj: Target object\r\n levels: Number of subdivision levels\r\n \r\n Returns:\r\n The multires modifier\r\n \"\"\"\r\n mod = obj.modifiers.new(\"Multires\", 'MULTIRES')\r\n \r\n bpy.context.view_layer.objects.active = obj\r\n for _ in range(levels):\n bpy.ops.object.multires_subdivide(modifier=\"Multires\")\r\n \r\n return mod","category":"utility","source_file":"sculpt_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Set Sculpt Brush. Category: utility","output":"import bpy\n\ndef set_sculpt_brush(brush_name: str) -> None:\r\n \"\"\"\r\n Set active sculpt brush.\r\n \r\n Args:\r\n brush_name: 'Draw', 'Clay', 'Clay Strips', 'Grab', 'Smooth',\r\n 'Flatten', 'Crease', 'Inflate', 'Pinch', 'Mask'\r\n \"\"\"\r\n brush = bpy.data.brushes.get(brush_name)\r\n if brush:\r\n bpy.context.tool_settings.sculpt.brush = brush\r\n\r\n\r\ndef configure_brush(\r\n strength: float = 0.5,\r\n radius: int = 50,\r\n auto_smooth: float = 0,\r\n use_frontface: bool = True\r\n) -> None:\r\n \"\"\"Configure active sculpt brush settings.\"\"\"\r\n brush = bpy.context.tool_settings.sculpt.brush\r\n if brush:\r\n brush.strength = strength\r\n brush.size = radius\r\n brush.auto_smooth_factor = auto_smooth\r\n brush.use_frontface = use_frontface\r\n\r\n\r\ndef mask_by_normal(\r\n limit: float = 0.5,\r\n direction: str = 'VIEW'\r\n) -> None:\r\n \"\"\"Create mask based on normal direction.\"\"\"\r\n bpy.ops.paint.mask_flood_fill(mode='VALUE', value=0)\r\n bpy.ops.sculpt.mask_by_normal(limit=limit)\r\n\r\n\r\ndef expand_mask(iterations: int = 1) -> None:\r\n \"\"\"Expand current mask.\"\"\"\r\n for _ in range(iterations):\n bpy.ops.sculpt.mask_expand(\r\n use_smooth=True,\r\n smooth_iterations=2\r\n )","category":"utility","source_file":"sculpt_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Invert Mask. Category: utility","output":"import bpy\n\ndef invert_mask() -> None:\r\n \"\"\"Invert mask.\"\"\"\r\n bpy.ops.paint.mask_flood_fill(mode='INVERT')\r\n\r\n\r\ndef clear_mask() -> None:\r\n \"\"\"Clear all masking.\"\"\"\r\n bpy.ops.paint.mask_flood_fill(mode='VALUE', value=0)\r\n\r\n\r\ndef hide_masked() -> None:\r\n \"\"\"Hide masked geometry.\"\"\"\r\n bpy.ops.sculpt.face_sets_create(mode='MASKED')\r\n bpy.ops.sculpt.face_set_change_visibility(mode='HIDE_ACTIVE')\r\n\r\n\r\ndef show_all() -> None:\r\n \"\"\"Show all hidden geometry.\"\"\"\r\n bpy.ops.sculpt.reveal_all()\r\n\r\n\r\ndef symmetrize_mesh(direction: str = 'NEGATIVE_X') -> None:\r\n \"\"\"\r\n Symmetrize mesh.\r\n \r\n Args:\r\n direction: 'NEGATIVE_X', 'POSITIVE_X', 'NEGATIVE_Y', etc.\r\n \"\"\"\r\n bpy.ops.sculpt.symmetrize(direction=direction)","category":"utility","source_file":"sculpt_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Object Selection Utilities. Helper functions to manage object selection state in the scene.","output":"import bpy\r\n\r\ndef select_all():\r\n bpy.ops.object.select_all(action='SELECT')\r\n\r\ndef deselect_all():\r\n bpy.ops.object.select_all(action='DESELECT')\r\n\r\ndef select_object(obj):\r\n obj.select_set(True)\r\n bpy.context.view_layer.objects.active = obj\r\n\r\ndef select_by_name(name):\r\n deselect_all()\r\n obj = bpy.data.objects.get(name)\r\n if obj:\r\n select_object(obj)\r\n return obj\r\n return None\r\n\r\ndef select_by_type(type='MESH'):\r\n deselect_all()\r\n for obj in bpy.context.scene.objects:\r\n if obj.type == type:\r\n obj.select_set(True)\r\n\r\ndef invert_selection():\r\n bpy.ops.object.select_all(action='INVERT')","category":"utility","source_file":"selection_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Select All. Category: utility","output":"import bpy\n\ndef select_all():\n bpy.ops.object.select_all(action='SELECT')","category":"utility","source_file":"selection_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Deselect All. Category: utility","output":"import bpy\n\ndef deselect_all():\n bpy.ops.object.select_all(action='DESELECT')","category":"utility","source_file":"selection_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Select Object. Category: utility","output":"import bpy\n\ndef select_object(obj):\n obj.select_set(True)\r\n bpy.context.view_layer.objects.active = obj","category":"utility","source_file":"selection_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Select By Name. Category: utility","output":"import bpy\n\ndef select_by_name(name):\n deselect_all()\r\n obj = bpy.data.objects.get(name)\r\n if obj:\r\n select_object(obj)\r\n return obj\r\n return None","category":"utility","source_file":"selection_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Select By Type. Category: utility","output":"import bpy\n\ndef select_by_type(type='MESH'):\n deselect_all()\r\n for obj in bpy.context.scene.objects:\r\n if obj.type == type:\r\n obj.select_set(True)","category":"utility","source_file":"selection_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Invert Selection. Category: utility","output":"import bpy\n\ndef invert_selection():\n bpy.ops.object.select_all(action='INVERT')","category":"utility","source_file":"selection_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Shader Node Utilities. Functions for creating and manipulating shader nodes.","output":"import bpy\r\n\r\n\r\ndef get_material_nodes(\r\n material: bpy.types.Material\r\n) -> tuple:\r\n \"\"\"\r\n Get material node tree components.\r\n \r\n Returns:\r\n Tuple of (nodes, links)\r\n \"\"\"\r\n return material.node_tree.nodes, material.node_tree.links\r\n\r\n\r\ndef add_node(\r\n material: bpy.types.Material,\r\n node_type: str,\r\n location: tuple = (0, 0),\r\n name: str = None\r\n) -> bpy.types.Node:\r\n \"\"\"\r\n Add node to material.\r\n \r\n Args:\r\n material: Target material\r\n node_type: Node type (e.g., 'ShaderNodeTexNoise')\r\n location: Node position\r\n name: Optional node name\r\n \r\n Returns:\r\n Created node\r\n \"\"\"\r\n nodes, _ = get_material_nodes(material)\r\n node = nodes.new(node_type)\r\n node.location = location\r\n if name:\r\n node.name = name\r\n return node\r\n\r\n\r\ndef connect_nodes(\r\n material: bpy.types.Material,\r\n from_node: str,\r\n from_socket: str,\r\n to_node: str,\r\n to_socket: str\r\n) -> None:\r\n \"\"\"\r\n Connect two nodes.\r\n \r\n Args:\r\n material: Target material\r\n from_node: Source node name\r\n from_socket: Output socket name\r\n to_node: Destination node name\r\n to_socket: Input socket name\r\n \"\"\"\r\n nodes, links = get_material_nodes(material)\r\n \r\n source = nodes.get(from_node)\r\n dest = nodes.get(to_node)\r\n \r\n if source and dest:\r\n links.new(source.outputs[from_socket], dest.inputs[to_socket])\r\n\r\n\r\ndef add_mix_rgb(\r\n material: bpy.types.Material,\r\n blend_type: str = 'MIX',\r\n fac: float = 0.5,\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"\r\n Add Mix RGB node.\r\n \r\n Args:\r\n blend_type: 'MIX', 'ADD', 'MULTIPLY', 'SCREEN', 'OVERLAY', etc.\r\n fac: Mix factor\r\n \"\"\"\r\n node = add_node(material, 'ShaderNodeMix', location)\r\n node.data_type = 'RGBA'\r\n node.blend_type = blend_type\r\n node.inputs['Factor'].default_value = fac\r\n return node\r\n\r\n\r\ndef add_color_ramp(\r\n material: bpy.types.Material,\r\n colors: list = None,\r\n positions: list = None,\r\n interpolation: str = 'LINEAR',\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"\r\n Add Color Ramp node.\r\n \r\n Args:\r\n colors: List of RGBA colors\r\n positions: List of positions (0-1)\r\n interpolation: 'LINEAR', 'CONSTANT', 'EASE'\r\n \"\"\"\r\n node = add_node(material, 'ShaderNodeValToRGB', location)\r\n node.color_ramp.interpolation = interpolation\r\n \r\n if colors and positions:\r\n ramp = node.color_ramp\r\n # Clear default elements\r\n while len(ramp.elements) > len(colors):\r\n ramp.elements.remove(ramp.elements[-1])\r\n while len(ramp.elements) < len(colors):\r\n ramp.elements.new(0)\r\n \r\n for i, (color, pos) in enumerate(zip(colors, positions)):\r\n ramp.elements[i].position = pos\r\n ramp.elements[i].color = color\r\n \r\n return node\r\n\r\n\r\ndef add_noise_texture(\r\n material: bpy.types.Material,\r\n scale: float = 5.0,\r\n detail: float = 2.0,\r\n roughness: float = 0.5,\r\n noise_type: str = 'FBM',\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"Add Noise Texture node.\"\"\"\r\n node = add_node(material, 'ShaderNodeTexNoise', location)\r\n node.inputs['Scale'].default_value = scale\r\n node.inputs['Detail'].default_value = detail\r\n node.inputs['Roughness'].default_value = roughness\r\n node.noise_dimensions = '3D'\r\n return node\r\n\r\n\r\ndef add_voronoi_texture(\r\n material: bpy.types.Material,\r\n scale: float = 5.0,\r\n feature: str = 'F1',\r\n distance: str = 'EUCLIDEAN',\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"Add Voronoi Texture node.\"\"\"\r\n node = add_node(material, 'ShaderNodeTexVoronoi', location)\r\n node.inputs['Scale'].default_value = scale\r\n node.feature = feature\r\n node.distance = distance\r\n return node\r\n\r\n\r\ndef add_wave_texture(\r\n material: bpy.types.Material,\r\n scale: float = 5.0,\r\n distortion: float = 0.0,\r\n wave_type: str = 'BANDS',\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"Add Wave Texture node.\"\"\"\r\n node = add_node(material, 'ShaderNodeTexWave', location)\r\n node.inputs['Scale'].default_value = scale\r\n node.inputs['Distortion'].default_value = distortion\r\n node.wave_type = wave_type\r\n return node\r\n\r\n\r\ndef add_image_texture(\r\n material: bpy.types.Material,\r\n image_path: str = None,\r\n image: bpy.types.Image = None,\r\n extension: str = 'REPEAT',\r\n interpolation: str = 'Linear',\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"Add Image Texture node.\"\"\"\r\n node = add_node(material, 'ShaderNodeTexImage', location)\r\n \r\n if image_path:\r\n node.image = bpy.data.images.load(image_path)\r\n elif image:\r\n node.image = image\r\n \r\n node.extension = extension\r\n node.interpolation = interpolation\r\n \r\n return node\r\n\r\n\r\ndef add_mapping_node(\r\n material: bpy.types.Material,\r\n location_offset: tuple = (0, 0, 0),\r\n rotation: tuple = (0, 0, 0),\r\n scale: tuple = (1, 1, 1),\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"Add Mapping node for texture coordinates.\"\"\"\r\n node = add_node(material, 'ShaderNodeMapping', location)\r\n node.inputs['Location'].default_value = location_offset\r\n node.inputs['Rotation'].default_value = rotation\r\n node.inputs['Scale'].default_value = scale\r\n return node\r\n\r\n\r\ndef add_tex_coordinate_node(\r\n material: bpy.types.Material,\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"Add Texture Coordinate node.\"\"\"\r\n return add_node(material, 'ShaderNodeTexCoord', location)\r\n\r\n\r\ndef add_bump_node(\r\n material: bpy.types.Material,\r\n strength: float = 1.0,\r\n distance: float = 1.0,\r\n invert: bool = False,\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"Add Bump node.\"\"\"\r\n node = add_node(material, 'ShaderNodeBump', location)\r\n node.inputs['Strength'].default_value = strength\r\n node.inputs['Distance'].default_value = distance\r\n node.invert = invert\r\n return node\r\n\r\n\r\ndef add_normal_map_node(\r\n material: bpy.types.Material,\r\n strength: float = 1.0,\r\n space: str = 'TANGENT',\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"Add Normal Map node.\"\"\"\r\n node = add_node(material, 'ShaderNodeNormalMap', location)\r\n node.inputs['Strength'].default_value = strength\r\n node.space = space\r\n return node\r\n\r\n\r\ndef add_math_node(\r\n material: bpy.types.Material,\r\n operation: str = 'ADD',\r\n value1: float = 0.0,\r\n value2: float = 0.0,\r\n clamp: bool = False,\r\n location: tuple = (0, 0)\r\n) -> bpy.types.Node:\r\n \"\"\"\r\n Add Math node.\r\n \r\n Args:\r\n operation: 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'POWER',\r\n 'SINE', 'COSINE', 'MINIMUM', 'MAXIMUM', etc.\r\n \"\"\"\r\n node = add_node(material, 'ShaderNodeMath', location)\r\n node.operation = operation\r\n node.inputs[0].default_value = value1\r\n node.inputs[1].default_value = value2\r\n node.use_clamp = clamp\r\n return node\r\n\r\n\r\ndef frame_nodes(\r\n material: bpy.types.Material,\r\n node_names: list,\r\n label: str = \"Group\"\r\n) -> bpy.types.Node:\r\n \"\"\"Create frame around nodes.\"\"\"\r\n nodes, _ = get_material_nodes(material)\r\n frame = nodes.new('NodeFrame')\r\n frame.label = label\r\n \r\n for name in node_names:\r\n node = nodes.get(name)\r\n if node:\r\n node.parent = frame\r\n \r\n return frame","category":"utility","source_file":"shader_node_utils.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Shape Key Utilities. Functions for creating and managing shape keys for mesh deformation.","output":"import bpy\r\n\r\n\r\ndef add_basis_shape_key(obj: bpy.types.Object) -> bpy.types.ShapeKey:\r\n \"\"\"Add basis (reference) shape key to mesh.\"\"\"\r\n if obj.data.shape_keys is None:\r\n obj.shape_key_add(name='Basis', from_mix=False)\r\n return obj.data.shape_keys.key_blocks['Basis']\r\n\r\n\r\ndef add_shape_key(\r\n obj: bpy.types.Object,\r\n name: str,\r\n from_mix: bool = False\r\n) -> bpy.types.ShapeKey:\r\n \"\"\"\r\n Add a new shape key.\r\n \r\n Args:\r\n obj: Target mesh object\r\n name: Shape key name\r\n from_mix: Create from current mix of all keys\r\n \r\n Returns:\r\n The created shape key\r\n \"\"\"\r\n if obj.data.shape_keys is None:\r\n add_basis_shape_key(obj)\r\n \r\n key = obj.shape_key_add(name=name, from_mix=from_mix)\r\n return key\r\n\r\n\r\ndef set_shape_key_value(\r\n obj: bpy.types.Object,\r\n name: str,\r\n value: float\r\n) -> None:\r\n \"\"\"Set shape key value (0-1).\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n obj.data.shape_keys.key_blocks[name].value = value\r\n\r\n\r\ndef get_shape_key_value(obj: bpy.types.Object, name: str) -> float:\r\n \"\"\"Get shape key value.\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n return obj.data.shape_keys.key_blocks[name].value\r\n return 0.0\r\n\r\n\r\ndef set_shape_key_range(\r\n obj: bpy.types.Object,\r\n name: str,\r\n min_val: float = 0.0,\r\n max_val: float = 1.0\r\n) -> None:\r\n \"\"\"Set shape key value range.\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n key = obj.data.shape_keys.key_blocks[name]\r\n key.slider_min = min_val\r\n key.slider_max = max_val\r\n\r\n\r\ndef keyframe_shape_key(\r\n obj: bpy.types.Object,\r\n name: str,\r\n value: float,\r\n frame: int\r\n) -> None:\r\n \"\"\"Insert keyframe for shape key.\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n key = obj.data.shape_keys.key_blocks[name]\r\n key.value = value\r\n key.keyframe_insert(data_path='value', frame=frame)\r\n\r\n\r\ndef create_morph_animation(\r\n obj: bpy.types.Object,\r\n shape_name: str,\r\n start_frame: int = 1,\r\n peak_frame: int = 12,\r\n end_frame: int = 24\r\n) -> None:\r\n \"\"\"\r\n Create a morph in/out animation.\r\n \r\n Args:\r\n obj: Target mesh\r\n shape_name: Shape key name\r\n start_frame: Animation start\r\n peak_frame: Full morph frame\r\n end_frame: Animation end\r\n \"\"\"\r\n keyframe_shape_key(obj, shape_name, 0.0, start_frame)\r\n keyframe_shape_key(obj, shape_name, 1.0, peak_frame)\r\n keyframe_shape_key(obj, shape_name, 0.0, end_frame)\r\n\r\n\r\ndef copy_shape_key_from_object(\r\n target: bpy.types.Object,\r\n source: bpy.types.Object,\r\n key_name: str,\r\n new_name: str = None\r\n) -> bpy.types.ShapeKey:\r\n \"\"\"\r\n Copy shape key from another object.\r\n \r\n Args:\r\n target: Object to receive shape key\r\n source: Object with source shape key\r\n key_name: Name of key to copy\r\n new_name: Name for copied key (uses original if None)\r\n \r\n Returns:\r\n The new shape key\r\n \"\"\"\r\n if source.data.shape_keys is None:\r\n return None\r\n \r\n source_key = source.data.shape_keys.key_blocks.get(key_name)\r\n if source_key is None:\r\n return None\r\n \r\n # Add basis if needed\r\n if target.data.shape_keys is None:\r\n add_basis_shape_key(target)\r\n \r\n # Create new key\r\n new_key = target.shape_key_add(name=new_name or key_name, from_mix=False)\r\n \r\n # Copy vertex positions\r\n for i, (sv, tv) in enumerate(zip(source_key.data, new_key.data)):\r\n tv.co = sv.co.copy()\r\n \r\n return new_key\r\n\r\n\r\ndef remove_shape_key(obj: bpy.types.Object, name: str) -> None:\r\n \"\"\"Remove shape key by name.\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n key = obj.data.shape_keys.key_blocks[name]\r\n obj.shape_key_remove(key)\r\n\r\n\r\ndef remove_all_shape_keys(obj: bpy.types.Object) -> None:\r\n \"\"\"Remove all shape keys.\"\"\"\r\n if obj.data.shape_keys:\r\n obj.shape_key_clear()\r\n\r\n\r\ndef list_shape_keys(obj: bpy.types.Object) -> list:\r\n \"\"\"Return list of shape key names.\"\"\"\r\n if obj.data.shape_keys:\r\n return [k.name for k in obj.data.shape_keys.key_blocks]\r\n return []","category":"utility","source_file":"shape_key_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Add Basis Shape Key. Category: utility","output":"import bpy\n\ndef add_basis_shape_key(obj: bpy.types.Object) -> bpy.types.ShapeKey:\r\n \"\"\"Add basis (reference) shape key to mesh.\"\"\"\r\n if obj.data.shape_keys is None:\r\n obj.shape_key_add(name='Basis', from_mix=False)\r\n return obj.data.shape_keys.key_blocks['Basis']\r\n\r\n\r\ndef add_shape_key(\r\n obj: bpy.types.Object,\r\n name: str,\r\n from_mix: bool = False\r\n) -> bpy.types.ShapeKey:\r\n \"\"\"\r\n Add a new shape key.\r\n \r\n Args:\r\n obj: Target mesh object\r\n name: Shape key name\r\n from_mix: Create from current mix of all keys\r\n \r\n Returns:\r\n The created shape key\r\n \"\"\"\r\n if obj.data.shape_keys is None:\r\n add_basis_shape_key(obj)\r\n \r\n key = obj.shape_key_add(name=name, from_mix=from_mix)\r\n return key\r\n\r\n\r\ndef set_shape_key_value(\r\n obj: bpy.types.Object,\r\n name: str,\r\n value: float\r\n) -> None:\r\n \"\"\"Set shape key value (0-1).\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n obj.data.shape_keys.key_blocks[name].value = value\r\n\r\n\r\ndef get_shape_key_value(obj: bpy.types.Object, name: str) -> float:\r\n \"\"\"Get shape key value.\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n return obj.data.shape_keys.key_blocks[name].value\r\n return 0.0\r\n\r\n\r\ndef set_shape_key_range(\r\n obj: bpy.types.Object,\r\n name: str,\r\n min_val: float = 0.0,\r\n max_val: float = 1.0\r\n) -> None:\r\n \"\"\"Set shape key value range.\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n key = obj.data.shape_keys.key_blocks[name]\r\n key.slider_min = min_val\r\n key.slider_max = max_val\r\n\r\n\r\ndef keyframe_shape_key(\r\n obj: bpy.types.Object,\r\n name: str,\r\n value: float,\r\n frame: int\r\n) -> None:\r\n \"\"\"Insert keyframe for shape key.\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n key = obj.data.shape_keys.key_blocks[name]\r\n key.value = value\r\n key.keyframe_insert(data_path='value', frame=frame)\r\n\r\n\r\ndef create_morph_animation(\r\n obj: bpy.types.Object,\r\n shape_name: str,\r\n start_frame: int = 1,\r\n peak_frame: int = 12,\r\n end_frame: int = 24\r\n) -> None:\r\n \"\"\"\r\n Create a morph in/out animation.\r\n \r\n Args:\r\n obj: Target mesh\r\n shape_name: Shape key name\r\n start_frame: Animation start\r\n peak_frame: Full morph frame\r\n end_frame: Animation end\r\n \"\"\"\r\n keyframe_shape_key(obj, shape_name, 0.0, start_frame)\r\n keyframe_shape_key(obj, shape_name, 1.0, peak_frame)\r\n keyframe_shape_key(obj, shape_name, 0.0, end_frame)\r\n\r\n\r\ndef copy_shape_key_from_object(\r\n target: bpy.types.Object,\r\n source: bpy.types.Object,\r\n key_name: str,\r\n new_name: str = None\r\n) -> bpy.types.ShapeKey:\r\n \"\"\"\r\n Copy shape key from another object.\r\n \r\n Args:\r\n target: Object to receive shape key\r\n source: Object with source shape key\r\n key_name: Name of key to copy\r\n new_name: Name for copied key (uses original if None)\r\n \r\n Returns:\r\n The new shape key\r\n \"\"\"\r\n if source.data.shape_keys is None:\r\n return None\r\n \r\n source_key = source.data.shape_keys.key_blocks.get(key_name)\r\n if source_key is None:\r\n return None\r\n \r\n # Add basis if needed\r\n if target.data.shape_keys is None:\r\n add_basis_shape_key(target)\r\n \r\n # Create new key\r\n new_key = target.shape_key_add(name=new_name or key_name, from_mix=False)\r\n \r\n # Copy vertex positions\r\n for i, (sv, tv) in enumerate(zip(source_key.data, new_key.data)):\n tv.co = sv.co.copy()\r\n \r\n return new_key","category":"utility","source_file":"shape_key_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Remove Shape Key. Category: utility","output":"import bpy\n\ndef remove_shape_key(obj: bpy.types.Object, name: str) -> None:\r\n \"\"\"Remove shape key by name.\"\"\"\r\n if obj.data.shape_keys and name in obj.data.shape_keys.key_blocks:\r\n key = obj.data.shape_keys.key_blocks[name]\r\n obj.shape_key_remove(key)\r\n\r\n\r\ndef remove_all_shape_keys(obj: bpy.types.Object) -> None:\r\n \"\"\"Remove all shape keys.\"\"\"\r\n if obj.data.shape_keys:\r\n obj.shape_key_clear()\r\n\r\n\r\ndef list_shape_keys(obj: bpy.types.Object) -> list:\r\n \"\"\"Return list of shape key names.\"\"\"\r\n if obj.data.shape_keys:\r\n return [k.name for k in obj.data.shape_keys.key_blocks]\r\n return []","category":"utility","source_file":"shape_key_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Camera Rig Presets. Pre-configured camera rigs for common cinematography setups.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_orbit_rig(\r\n target_location: tuple = (0, 0, 0),\r\n radius: float = 5.0,\r\n height: float = 2.0,\r\n name: str = \"OrbitRig\"\r\n) -> dict:\r\n \"\"\"\r\n Create an orbital camera rig.\r\n \r\n Args:\r\n target_location: Point to orbit around\r\n radius: Orbit radius\r\n height: Camera height above target\r\n name: Rig name\r\n \r\n Returns:\r\n Dictionary with rig components\r\n \"\"\"\r\n result = {}\r\n \r\n # Pivot empty\r\n bpy.ops.object.empty_add(\r\n type='PLAIN_AXES',\r\n location=target_location\r\n )\r\n pivot = bpy.context.active_object\r\n pivot.name = f\"{name}_Pivot\"\r\n result['pivot'] = pivot\r\n \r\n # Camera\r\n bpy.ops.object.camera_add(location=(\r\n target_location[0] + radius,\r\n target_location[1],\r\n target_location[2] + height\r\n ))\r\n camera = bpy.context.active_object\r\n camera.name = f\"{name}_Camera\"\r\n camera.parent = pivot\r\n \r\n # Track to target\r\n constraint = camera.constraints.new('TRACK_TO')\r\n constraint.target = pivot\r\n constraint.track_axis = 'TRACK_NEGATIVE_Z'\r\n constraint.up_axis = 'UP_Y'\r\n \r\n result['camera'] = camera\r\n \r\n return result\r\n\r\n\r\ndef create_dolly_rig(\r\n start: tuple = (-5, 0, 1.6),\r\n end: tuple = (5, 0, 1.6),\r\n target: tuple = (0, 0, 1),\r\n name: str = \"DollyRig\"\r\n) -> dict:\r\n \"\"\"\r\n Create a dolly track rig.\r\n \r\n Args:\r\n start: Track start position\r\n end: Track end position\r\n target: Look-at target\r\n name: Rig name\r\n \r\n Returns:\r\n Dictionary with rig components\r\n \"\"\"\r\n result = {}\r\n \r\n # Create path\r\n curve_data = bpy.data.curves.new(f\"{name}_Track\", 'CURVE')\r\n curve_data.dimensions = '3D'\r\n spline = curve_data.splines.new('NURBS')\r\n spline.points.add(1)\r\n spline.points[0].co = (*start, 1)\r\n spline.points[1].co = (*end, 1)\r\n \r\n track = bpy.data.objects.new(f\"{name}_Track\", curve_data)\r\n bpy.context.collection.objects.link(track)\r\n result['track'] = track\r\n \r\n # Target empty\r\n bpy.ops.object.empty_add(type='SPHERE', location=target)\r\n target_obj = bpy.context.active_object\r\n target_obj.name = f\"{name}_Target\"\r\n result['target'] = target_obj\r\n \r\n # Camera\r\n bpy.ops.object.camera_add(location=start)\r\n camera = bpy.context.active_object\r\n camera.name = f\"{name}_Camera\"\r\n \r\n # Follow path\r\n follow = camera.constraints.new('FOLLOW_PATH')\r\n follow.target = track\r\n follow.use_curve_follow = False\r\n \r\n # Track to target\r\n track_to = camera.constraints.new('TRACK_TO')\r\n track_to.target = target_obj\r\n track_to.track_axis = 'TRACK_NEGATIVE_Z'\r\n track_to.up_axis = 'UP_Y'\r\n \r\n result['camera'] = camera\r\n \r\n return result\r\n\r\n\r\ndef create_crane_rig(\r\n base_location: tuple = (0, -5, 0),\r\n arm_length: float = 5.0,\r\n height: float = 3.0,\r\n name: str = \"CraneRig\"\r\n) -> dict:\r\n \"\"\"\r\n Create a crane/jib camera rig.\r\n \r\n Args:\r\n base_location: Crane base position\r\n arm_length: Arm length\r\n height: Arm pivot height\r\n name: Rig name\r\n \r\n Returns:\r\n Dictionary with rig components\r\n \"\"\"\r\n result = {}\r\n \r\n # Base (rotation)\r\n bpy.ops.object.empty_add(type='CIRCLE', location=base_location)\r\n base = bpy.context.active_object\r\n base.name = f\"{name}_Base\"\r\n result['base'] = base\r\n \r\n # Arm pivot\r\n bpy.ops.object.empty_add(\r\n type='ARROWS',\r\n location=(base_location[0], base_location[1], base_location[2] + height)\r\n )\r\n arm = bpy.context.active_object\r\n arm.name = f\"{name}_Arm\"\r\n arm.parent = base\r\n result['arm'] = arm\r\n \r\n # Camera at end of arm\r\n bpy.ops.object.camera_add(location=(\r\n base_location[0],\r\n base_location[1] + arm_length,\r\n base_location[2] + height\r\n ))\r\n camera = bpy.context.active_object\r\n camera.name = f\"{name}_Camera\"\r\n camera.parent = arm\r\n \r\n result['camera'] = camera\r\n \r\n return result\r\n\r\n\r\ndef create_handheld_rig(\r\n location: tuple = (0, -3, 1.6),\r\n shake_intensity: float = 0.3,\r\n name: str = \"HandheldRig\"\r\n) -> dict:\r\n \"\"\"\r\n Create handheld camera rig with noise.\r\n \r\n Args:\r\n location: Camera position\r\n shake_intensity: Shake amount\r\n name: Rig name\r\n \r\n Returns:\r\n Dictionary with rig components\r\n \"\"\"\r\n result = {}\r\n \r\n # Stabilizer empty\r\n bpy.ops.object.empty_add(type='PLAIN_AXES', location=location)\r\n stabilizer = bpy.context.active_object\r\n stabilizer.name = f\"{name}_Stabilizer\"\r\n result['stabilizer'] = stabilizer\r\n \r\n # Camera\r\n bpy.ops.object.camera_add(location=location)\r\n camera = bpy.context.active_object\r\n camera.name = f\"{name}_Camera\"\r\n camera.parent = stabilizer\r\n \r\n # Add noise drivers for shake\r\n for i, axis in enumerate(['x', 'y', 'z']):\r\n strength = shake_intensity * (0.5 if axis == 'z' else 1.0)\r\n fcurve = camera.driver_add('rotation_euler', i)\r\n driver = fcurve.driver\r\n driver.expression = f\"noise.random() * {strength} * 0.02\"\r\n \r\n result['camera'] = camera\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_orbit_rig()\r\n create_dolly_rig(start=(-5, 5, 1.6), end=(5, 5, 1.6))\r\n \r\n print(\"Created camera rigs\")","category":"tasks/animation","source_file":"tasks\\animation\\camera_rigs.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Camera Shake Effect. Adds realistic camera shake and handheld motion effects.","output":"import bpy\r\nimport random\r\nimport math\r\n\r\n\r\ndef add_camera_shake(\r\n camera: bpy.types.Object,\r\n intensity: float = 0.5,\r\n frequency: float = 2.0,\r\n frame_start: int = 1,\r\n frame_end: int = 250,\r\n shake_type: str = 'HANDHELD',\r\n seed: int = 42\r\n) -> dict:\r\n \"\"\"\r\n Add camera shake animation.\r\n \r\n Args:\r\n camera: Camera object\r\n intensity: Shake strength (0-1)\r\n frequency: Shake speed\r\n frame_start: Start frame\r\n frame_end: End frame\r\n shake_type: 'HANDHELD', 'EARTHQUAKE', 'SUBTLE'\r\n seed: Random seed\r\n \r\n Returns:\r\n Dictionary with animation data\r\n \"\"\"\r\n random.seed(seed)\r\n \r\n # Intensity presets\r\n presets = {\r\n 'SUBTLE': {'pos': 0.002, 'rot': 0.2},\r\n 'HANDHELD': {'pos': 0.01, 'rot': 0.5},\r\n 'EARTHQUAKE': {'pos': 0.05, 'rot': 1.5}\r\n }\r\n \r\n preset = presets.get(shake_type, presets['HANDHELD'])\r\n pos_strength = preset['pos'] * intensity\r\n rot_strength = preset['rot'] * intensity\r\n \r\n # Store original location\r\n orig_loc = camera.location.copy()\r\n orig_rot = camera.rotation_euler.copy()\r\n \r\n # Generate keyframes\r\n for frame in range(frame_start, frame_end + 1):\r\n bpy.context.scene.frame_set(frame)\r\n \r\n t = frame * frequency * 0.1\r\n \r\n # Position noise (Perlin-like)\r\n camera.location.x = orig_loc.x + _noise(t, seed) * pos_strength\r\n camera.location.y = orig_loc.y + _noise(t + 100, seed) * pos_strength\r\n camera.location.z = orig_loc.z + _noise(t + 200, seed) * pos_strength * 0.5\r\n \r\n # Rotation noise (smaller)\r\n camera.rotation_euler.x = orig_rot.x + math.radians(_noise(t + 300, seed) * rot_strength)\r\n camera.rotation_euler.y = orig_rot.y + math.radians(_noise(t + 400, seed) * rot_strength * 0.5)\r\n camera.rotation_euler.z = orig_rot.z + math.radians(_noise(t + 500, seed) * rot_strength * 0.3)\r\n \r\n camera.keyframe_insert(data_path=\"location\", frame=frame)\r\n camera.keyframe_insert(data_path=\"rotation_euler\", frame=frame)\r\n \r\n # Smooth keyframes\r\n if camera.animation_data and camera.animation_data.action:\r\n for fcurve in camera.animation_data.action.fcurves:\r\n for kp in fcurve.keyframe_points:\r\n kp.interpolation = 'BEZIER'\r\n kp.handle_left_type = 'AUTO_CLAMPED'\r\n kp.handle_right_type = 'AUTO_CLAMPED'\r\n \r\n return {\r\n 'camera': camera.name,\r\n 'frames': (frame_start, frame_end),\r\n 'shake_type': shake_type\r\n }\r\n\r\n\r\ndef _noise(t: float, seed: int) -> float:\r\n \"\"\"Simple noise function.\"\"\"\r\n random.seed(int(t * 10) + seed)\r\n base = random.uniform(-1, 1)\r\n \r\n random.seed(int(t * 10) + 1 + seed)\r\n next_val = random.uniform(-1, 1)\r\n \r\n frac = (t * 10) % 1\r\n return base + (next_val - base) * frac\r\n\r\n\r\ndef add_camera_drift(\r\n camera: bpy.types.Object,\r\n amount: float = 0.1,\r\n speed: float = 0.5,\r\n frame_start: int = 1,\r\n frame_end: int = 250\r\n) -> None:\r\n \"\"\"Add subtle slow camera drift.\"\"\"\r\n orig_loc = camera.location.copy()\r\n \r\n for frame in range(frame_start, frame_end + 1, 10):\r\n bpy.context.scene.frame_set(frame)\r\n \r\n t = frame / (frame_end - frame_start)\r\n \r\n camera.location.x = orig_loc.x + math.sin(t * math.pi * speed) * amount\r\n camera.location.y = orig_loc.y + math.cos(t * math.pi * speed * 0.7) * amount * 0.5\r\n \r\n camera.keyframe_insert(data_path=\"location\", frame=frame)\r\n\r\n\r\ndef add_zoom_pulse(\r\n camera: bpy.types.Object,\r\n zoom_amount: float = 0.1,\r\n duration: int = 10,\r\n frame: int = 1\r\n) -> None:\r\n \"\"\"Add a quick zoom pulse effect.\"\"\"\r\n orig_lens = camera.data.lens\r\n \r\n camera.data.keyframe_insert(data_path=\"lens\", frame=frame)\r\n \r\n camera.data.lens = orig_lens * (1 - zoom_amount)\r\n camera.data.keyframe_insert(data_path=\"lens\", frame=frame + duration // 2)\r\n \r\n camera.data.lens = orig_lens\r\n camera.data.keyframe_insert(data_path=\"lens\", frame=frame + duration)\r\n \r\n # Make smooth\r\n if camera.data.animation_data and camera.data.animation_data.action:\r\n for fcurve in camera.data.animation_data.action.fcurves:\r\n for kp in fcurve.keyframe_points:\r\n kp.interpolation = 'BEZIER'\r\n\r\n\r\nif __name__ == \"__main__\":\r\n camera = bpy.context.scene.camera\r\n if camera:\r\n add_camera_shake(camera, intensity=0.3, shake_type='HANDHELD')\r\n print(\"Added camera shake\")\r\n else:\r\n print(\"No camera in scene\")","category":"tasks/animation","source_file":"tasks\\animation\\camera_shake.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Product Turntable Animation. Creates a smooth 360-degree turntable animation for product visualization.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_turntable_animation(\r\n target_object: bpy.types.Object = None,\r\n duration: int = 120,\r\n start_frame: int = 1,\r\n axis: str = 'Z',\r\n rotations: int = 1,\r\n ease_in_out: bool = False,\r\n name: str = None\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a smooth turntable rotation animation for an object.\r\n \r\n Args:\r\n target_object: Object to animate (uses active if None)\r\n duration: Animation length in frames\r\n start_frame: Starting frame number\r\n axis: Rotation axis ('X', 'Y', or 'Z')\r\n rotations: Number of full 360Β° rotations\r\n ease_in_out: Add easing at start/end\r\n name: Object to find by name (alternative to target_object)\r\n \r\n Returns:\r\n The animated object\r\n \r\n Example:\r\n >>> create_turntable_animation(bpy.data.objects['Product'], duration=90)\r\n \"\"\"\r\n if target_object is None:\r\n if name:\r\n target_object = bpy.data.objects.get(name)\r\n else:\r\n target_object = bpy.context.active_object\r\n \r\n if target_object is None:\r\n raise ValueError(\"No target object specified\")\r\n \r\n axis_index = {'X': 0, 'Y': 1, 'Z': 2}[axis.upper()]\r\n end_frame = start_frame + duration\r\n \r\n # Set frame range\r\n bpy.context.scene.frame_start = start_frame\r\n bpy.context.scene.frame_end = end_frame\r\n \r\n # Store initial rotation\r\n initial_rotation = target_object.rotation_euler[axis_index]\r\n \r\n # Keyframe start\r\n bpy.context.scene.frame_set(start_frame)\r\n target_object.rotation_euler[axis_index] = initial_rotation\r\n target_object.keyframe_insert(data_path='rotation_euler', index=axis_index, frame=start_frame)\r\n \r\n # Keyframe end\r\n target_object.rotation_euler[axis_index] = initial_rotation + (math.pi * 2 * rotations)\r\n target_object.keyframe_insert(data_path='rotation_euler', index=axis_index, frame=end_frame)\r\n \r\n # Set interpolation\r\n if target_object.animation_data and target_object.animation_data.action:\r\n for fcurve in target_object.animation_data.action.fcurves:\r\n if fcurve.data_path == 'rotation_euler' and fcurve.array_index == axis_index:\r\n for keyframe in fcurve.keyframe_points:\r\n if ease_in_out:\r\n keyframe.interpolation = 'BEZIER'\r\n keyframe.easing = 'EASE_IN_OUT'\r\n else:\r\n keyframe.interpolation = 'LINEAR'\r\n \r\n # Reset to start\r\n bpy.context.scene.frame_set(start_frame)\r\n \r\n return target_object\r\n\r\n\r\ndef create_turntable_with_camera(\r\n target_object: bpy.types.Object = None,\r\n camera_distance: float = 5.0,\r\n camera_height: float = 2.0,\r\n duration: int = 120,\r\n orbit_camera: bool = True\r\n) -> dict:\r\n \"\"\"\r\n Create a turntable setup with orbiting camera.\r\n \r\n Args:\r\n target_object: Object to showcase\r\n camera_distance: Distance from object center\r\n camera_height: Camera height above object\r\n duration: Animation duration in frames\r\n orbit_camera: If True, camera orbits; if False, object rotates\r\n \r\n Returns:\r\n Dictionary with camera and animation info\r\n \r\n Example:\r\n >>> setup = create_turntable_with_camera(product, camera_distance=8)\r\n \"\"\"\r\n result = {}\r\n \r\n if target_object is None:\r\n target_object = bpy.context.active_object\r\n \r\n obj_location = target_object.location\r\n \r\n # Create camera\r\n cam_x = obj_location.x + camera_distance\r\n cam_y = obj_location.y\r\n cam_z = obj_location.z + camera_height\r\n \r\n bpy.ops.object.camera_add(location=(cam_x, cam_y, cam_z))\r\n camera = bpy.context.active_object\r\n camera.name = \"TurntableCamera\"\r\n \r\n # Point camera at object\r\n from mathutils import Vector\r\n direction = Vector(obj_location) - camera.location\r\n rot_quat = direction.to_track_quat('-Z', 'Y')\r\n camera.rotation_euler = rot_quat.to_euler()\r\n \r\n # Set as active camera\r\n bpy.context.scene.camera = camera\r\n result['camera'] = camera\r\n \r\n if orbit_camera:\r\n # Create empty at object center for camera to orbit around\r\n bpy.ops.object.empty_add(type='PLAIN_AXES', location=obj_location)\r\n pivot = bpy.context.active_object\r\n pivot.name = \"TurntablePivot\"\r\n \r\n # Parent camera to pivot\r\n camera.parent = pivot\r\n \r\n # Animate pivot rotation\r\n create_turntable_animation(\r\n target_object=pivot,\r\n duration=duration,\r\n axis='Z'\r\n )\r\n result['pivot'] = pivot\r\n else:\r\n # Animate object directly\r\n create_turntable_animation(\r\n target_object=target_object,\r\n duration=duration,\r\n axis='Z'\r\n )\r\n \r\n result['target'] = target_object\r\n result['duration'] = duration\r\n \r\n return result\r\n\r\n\r\ndef setup_turntable_render(\r\n output_path: str = \"//turntable_\",\r\n format: str = 'PNG',\r\n resolution: tuple = (1920, 1080)\r\n) -> None:\r\n \"\"\"\r\n Configure render settings for turntable animation.\r\n \r\n Args:\r\n output_path: Output file path (// = relative to .blend)\r\n format: 'PNG', 'JPEG', 'FFMPEG' (video)\r\n resolution: (width, height) in pixels\r\n \r\n Example:\r\n >>> setup_turntable_render(\"//renders/product_\", 'PNG', (4096, 4096))\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n scene.render.resolution_x = resolution[0]\r\n scene.render.resolution_y = resolution[1]\r\n scene.render.resolution_percentage = 100\r\n \r\n scene.render.filepath = output_path\r\n scene.render.image_settings.file_format = format\r\n \r\n if format == 'FFMPEG':\r\n scene.render.ffmpeg.format = 'MPEG4'\r\n scene.render.ffmpeg.codec = 'H264'\r\n scene.render.ffmpeg.constant_rate_factor = 'HIGH'\r\n \r\n # Enable transparency for PNG\r\n if format == 'PNG':\r\n scene.render.film_transparent = True\r\n scene.render.image_settings.color_mode = 'RGBA'\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n # Get selected object\r\n obj = bpy.context.active_object\r\n if obj:\r\n setup = create_turntable_with_camera(\r\n target_object=obj,\r\n camera_distance=6,\r\n duration=90\r\n )\r\n setup_turntable_render(\"//turntable_\", 'PNG')\r\n print(\"Turntable animation created! Render with Ctrl+F12\")","category":"tasks/animation","source_file":"tasks\\animation\\turntable.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Walk Cycle Animation. Creates a procedural walk cycle animation for a humanoid armature.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_walk_cycle(\r\n armature: bpy.types.Object,\r\n step_length: float = 0.5,\r\n step_height: float = 0.15,\r\n cycle_frames: int = 24,\r\n hip_sway: float = 0.05,\r\n arm_swing: float = 0.4,\r\n start_frame: int = 1\r\n) -> dict:\r\n \"\"\"\r\n Create a basic walk cycle animation.\r\n \r\n Args:\r\n armature: Armature object with standard bone names\r\n step_length: Distance of each step\r\n step_height: How high feet lift\r\n cycle_frames: Frames per complete cycle\r\n hip_sway: Side-to-side hip movement\r\n arm_swing: Arm swing amplitude\r\n start_frame: Starting frame\r\n \r\n Returns:\r\n Dictionary with animation info\r\n \r\n Example:\r\n >>> create_walk_cycle(bpy.data.objects['Armature'])\r\n \"\"\"\r\n if armature.type != 'ARMATURE':\r\n raise ValueError(\"Object must be an armature\")\r\n \r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n result = {\r\n 'armature': armature.name,\r\n 'start_frame': start_frame,\r\n 'end_frame': start_frame + cycle_frames - 1,\r\n 'cycle_frames': cycle_frames\r\n }\r\n \r\n # Get pose bones - try common naming conventions\r\n bone_mapping = _find_bones(armature)\r\n \r\n if not bone_mapping.get('spine'):\r\n print(\"Warning: Could not find standard bone naming. Animation may not work.\")\r\n return result\r\n \r\n # Create keyframes\r\n half = cycle_frames // 2\r\n quarter = cycle_frames // 4\r\n \r\n for frame_offset in range(cycle_frames):\r\n frame = start_frame + frame_offset\r\n bpy.context.scene.frame_set(frame)\r\n \r\n # Normalized position in cycle (0-1)\r\n t = frame_offset / cycle_frames\r\n \r\n # === SPINE/HIP MOVEMENT ===\r\n if bone_mapping.get('spine'):\r\n spine = armature.pose.bones.get(bone_mapping['spine'])\r\n if spine:\r\n # Up/down bob\r\n spine.location.z = 0.03 * math.sin(t * 4 * math.pi)\r\n # Side to side sway\r\n spine.location.x = hip_sway * math.sin(t * 2 * math.pi)\r\n # Slight rotation\r\n spine.rotation_euler.y = math.radians(3) * math.sin(t * 2 * math.pi)\r\n spine.keyframe_insert(data_path=\"location\", frame=frame)\r\n spine.keyframe_insert(data_path=\"rotation_euler\", frame=frame)\r\n \r\n # === LEG MOVEMENT ===\r\n _animate_leg(\r\n armature, bone_mapping, 'left', \r\n t, step_length, step_height, frame, phase=0\r\n )\r\n _animate_leg(\r\n armature, bone_mapping, 'right', \r\n t, step_length, step_height, frame, phase=0.5\r\n )\r\n \r\n # === ARM SWING ===\r\n _animate_arm(armature, bone_mapping, 'left', t, arm_swing, frame, phase=0.5)\r\n _animate_arm(armature, bone_mapping, 'right', t, arm_swing, frame, phase=0)\r\n \r\n # Make cyclic\r\n _make_cyclic(armature)\r\n \r\n return result\r\n\r\n\r\ndef _find_bones(armature: bpy.types.Object) -> dict:\r\n \"\"\"Find bones using common naming conventions.\"\"\"\r\n mapping = {}\r\n bones = armature.pose.bones\r\n \r\n # Common naming patterns\r\n patterns = {\r\n 'spine': ['spine', 'spine.001', 'Spine', 'hips', 'Hips', 'pelvis'],\r\n 'thigh_l': ['thigh.L', 'Thigh.L', 'upperleg.L', 'UpperLeg.L', 'leg_upper.L'],\r\n 'thigh_r': ['thigh.R', 'Thigh.R', 'upperleg.R', 'UpperLeg.R', 'leg_upper.R'],\r\n 'shin_l': ['shin.L', 'Shin.L', 'lowerleg.L', 'LowerLeg.L', 'leg_lower.L', 'calf.L'],\r\n 'shin_r': ['shin.R', 'Shin.R', 'lowerleg.R', 'LowerLeg.R', 'leg_lower.R', 'calf.R'],\r\n 'foot_l': ['foot.L', 'Foot.L', 'ankle.L', 'Ankle.L'],\r\n 'foot_r': ['foot.R', 'Foot.R', 'ankle.R', 'Ankle.R'],\r\n 'upper_arm_l': ['upperarm.L', 'UpperArm.L', 'arm_upper.L', 'shoulder.L'],\r\n 'upper_arm_r': ['upperarm.R', 'UpperArm.R', 'arm_upper.R', 'shoulder.R'],\r\n 'forearm_l': ['forearm.L', 'Forearm.L', 'arm_lower.L', 'lowerarm.L'],\r\n 'forearm_r': ['forearm.R', 'Forearm.R', 'arm_lower.R', 'lowerarm.R'],\r\n }\r\n \r\n for bone_type, names in patterns.items():\r\n for name in names:\r\n if name in bones:\r\n mapping[bone_type] = name\r\n break\r\n \r\n return mapping\r\n\r\n\r\ndef _animate_leg(\r\n armature: bpy.types.Object,\r\n mapping: dict,\r\n side: str,\r\n t: float,\r\n step_length: float,\r\n step_height: float,\r\n frame: int,\r\n phase: float = 0\r\n) -> None:\r\n \"\"\"Animate one leg.\"\"\"\r\n suffix = 'l' if side == 'left' else 'r'\r\n \r\n # Adjust phase\r\n t_adj = (t + phase) % 1.0\r\n \r\n thigh = armature.pose.bones.get(mapping.get(f'thigh_{suffix}'))\r\n shin = armature.pose.bones.get(mapping.get(f'shin_{suffix}'))\r\n foot = armature.pose.bones.get(mapping.get(f'foot_{suffix}'))\r\n \r\n if thigh:\r\n # Thigh rotation (forward/back swing)\r\n swing = math.sin(t_adj * 2 * math.pi) * 0.4\r\n thigh.rotation_euler.x = swing\r\n thigh.keyframe_insert(data_path=\"rotation_euler\", frame=frame)\r\n \r\n if shin:\r\n # Knee bend (more bent during middle of step)\r\n bend = abs(math.sin(t_adj * 2 * math.pi)) * 0.6\r\n shin.rotation_euler.x = bend\r\n shin.keyframe_insert(data_path=\"rotation_euler\", frame=frame)\r\n \r\n if foot:\r\n # Foot rotation (toe-off and heel-strike)\r\n foot_rot = math.sin((t_adj + 0.25) * 2 * math.pi) * 0.3\r\n foot.rotation_euler.x = foot_rot\r\n foot.keyframe_insert(data_path=\"rotation_euler\", frame=frame)\r\n\r\n\r\ndef _animate_arm(\r\n armature: bpy.types.Object,\r\n mapping: dict,\r\n side: str,\r\n t: float,\r\n amplitude: float,\r\n frame: int,\r\n phase: float = 0\r\n) -> None:\r\n \"\"\"Animate one arm.\"\"\"\r\n suffix = 'l' if side == 'left' else 'r'\r\n \r\n t_adj = (t + phase) % 1.0\r\n \r\n upper_arm = armature.pose.bones.get(mapping.get(f'upper_arm_{suffix}'))\r\n forearm = armature.pose.bones.get(mapping.get(f'forearm_{suffix}'))\r\n \r\n if upper_arm:\r\n # Arm swing opposite to leg\r\n swing = math.sin(t_adj * 2 * math.pi) * amplitude\r\n upper_arm.rotation_euler.x = swing\r\n upper_arm.keyframe_insert(data_path=\"rotation_euler\", frame=frame)\r\n \r\n if forearm:\r\n # Forearm bend\r\n bend = 0.3 + abs(math.sin(t_adj * 2 * math.pi)) * 0.3\r\n forearm.rotation_euler.x = bend\r\n forearm.keyframe_insert(data_path=\"rotation_euler\", frame=frame)\r\n\r\n\r\ndef _make_cyclic(armature: bpy.types.Object) -> None:\r\n \"\"\"Make animation cyclic.\"\"\"\r\n if armature.animation_data and armature.animation_data.action:\r\n action = armature.animation_data.action\r\n for fcurve in action.fcurves:\r\n for mod in fcurve.modifiers:\r\n if mod.type == 'CYCLES':\r\n return # Already cyclic\r\n \r\n mod = fcurve.modifiers.new('CYCLES')\r\n mod.mode_before = 'REPEAT'\r\n mod.mode_after = 'REPEAT'\r\n\r\n\r\ndef create_idle_animation(\r\n armature: bpy.types.Object,\r\n cycle_frames: int = 60,\r\n breath_amount: float = 0.02,\r\n sway_amount: float = 0.01,\r\n start_frame: int = 1\r\n) -> dict:\r\n \"\"\"\r\n Create a subtle idle breathing/swaying animation.\r\n \r\n Args:\r\n armature: Character armature\r\n cycle_frames: Frames per breath cycle\r\n breath_amount: Chest expansion amount\r\n sway_amount: Body sway amount\r\n start_frame: Starting frame\r\n \r\n Returns:\r\n Dictionary with animation info\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n bone_mapping = _find_bones(armature)\r\n \r\n for frame_offset in range(cycle_frames):\r\n frame = start_frame + frame_offset\r\n bpy.context.scene.frame_set(frame)\r\n \r\n t = frame_offset / cycle_frames\r\n \r\n # Breathing (slow sine wave)\r\n breath = math.sin(t * 2 * math.pi) * breath_amount\r\n \r\n # Subtle sway\r\n sway = math.sin((t + 0.5) * 2 * math.pi) * sway_amount\r\n \r\n if bone_mapping.get('spine'):\r\n spine = armature.pose.bones.get(bone_mapping['spine'])\r\n if spine:\r\n spine.location.z = breath\r\n spine.location.x = sway\r\n spine.keyframe_insert(data_path=\"location\", frame=frame)\r\n \r\n _make_cyclic(armature)\r\n \r\n return {\r\n 'armature': armature.name,\r\n 'start_frame': start_frame,\r\n 'end_frame': start_frame + cycle_frames - 1,\r\n 'type': 'idle'\r\n }\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n # Find armature in scene\r\n armatures = [obj for obj in bpy.context.scene.objects if obj.type == 'ARMATURE']\r\n \r\n if armatures:\r\n result = create_walk_cycle(armatures[0], cycle_frames=24)\r\n print(f\"Created walk cycle: frames {result['start_frame']}-{result['end_frame']}\")\r\n else:\r\n print(\"No armature found in scene. Add an armature first.\")","category":"tasks/animation","source_file":"tasks\\animation\\walk_cycle.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Interior Room Generator. Creates a basic interior room with walls, floor, ceiling, and optional door/window openings.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_interior_room(\r\n width: float = 5.0,\r\n depth: float = 4.0,\r\n height: float = 2.8,\r\n wall_thickness: float = 0.15,\r\n location: tuple = (0, 0, 0),\r\n add_floor: bool = True,\r\n add_ceiling: bool = True,\r\n floor_material: str = 'WOOD',\r\n wall_color: tuple = (0.9, 0.9, 0.88),\r\n name_prefix: str = \"Room\"\r\n) -> dict:\r\n \"\"\"\r\n Create a basic interior room.\r\n \r\n Args:\r\n width: Room width (X axis)\r\n depth: Room depth (Y axis)\r\n height: Room height (Z axis)\r\n wall_thickness: Wall thickness\r\n location: Room base position\r\n add_floor: Create floor\r\n add_ceiling: Create ceiling\r\n floor_material: 'WOOD', 'TILE', 'CARPET', 'CONCRETE'\r\n wall_color: RGB wall color\r\n name_prefix: Prefix for object names\r\n \r\n Returns:\r\n Dictionary with all created objects\r\n \r\n Example:\r\n >>> room = create_interior_room(width=6, depth=5, height=3)\r\n \"\"\"\r\n result = {}\r\n bx, by, bz = location\r\n \r\n # === WALLS ===\r\n walls = _create_walls(width, depth, height, wall_thickness, location, wall_color, name_prefix)\r\n result.update(walls)\r\n \r\n # === FLOOR ===\r\n if add_floor:\r\n floor = _create_floor(width, depth, location, floor_material, name_prefix)\r\n result['floor'] = floor\r\n \r\n # === CEILING ===\r\n if add_ceiling:\r\n ceiling = _create_ceiling(width, depth, height, location, name_prefix)\r\n result['ceiling'] = ceiling\r\n \r\n return result\r\n\r\n\r\ndef _create_walls(\r\n width: float,\r\n depth: float,\r\n height: float,\r\n thickness: float,\r\n location: tuple,\r\n color: tuple,\r\n name_prefix: str\r\n) -> dict:\r\n \"\"\"Create the four walls.\"\"\"\r\n walls = {}\r\n bx, by, bz = location\r\n \r\n # Wall material\r\n wall_mat = bpy.data.materials.new(f\"{name_prefix}_WallMat\")\r\n bsdf = wall_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n \r\n # Back wall (positive Y)\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(bx, by + depth/2 - thickness/2, bz + height/2))\r\n back = bpy.context.active_object\r\n back.name = f\"{name_prefix}_Wall_Back\"\r\n back.scale = (width/2, thickness/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n back.data.materials.append(wall_mat)\r\n walls['back'] = back\r\n \r\n # Front wall (negative Y)\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(bx, by - depth/2 + thickness/2, bz + height/2))\r\n front = bpy.context.active_object\r\n front.name = f\"{name_prefix}_Wall_Front\"\r\n front.scale = (width/2, thickness/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n front.data.materials.append(wall_mat)\r\n walls['front'] = front\r\n \r\n # Left wall (negative X)\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(bx - width/2 + thickness/2, by, bz + height/2))\r\n left = bpy.context.active_object\r\n left.name = f\"{name_prefix}_Wall_Left\"\r\n left.scale = (thickness/2, depth/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n left.data.materials.append(wall_mat)\r\n walls['left'] = left\r\n \r\n # Right wall (positive X)\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(bx + width/2 - thickness/2, by, bz + height/2))\r\n right = bpy.context.active_object\r\n right.name = f\"{name_prefix}_Wall_Right\"\r\n right.scale = (thickness/2, depth/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n right.data.materials.append(wall_mat)\r\n walls['right'] = right\r\n \r\n return walls\r\n\r\n\r\ndef _create_floor(\r\n width: float,\r\n depth: float,\r\n location: tuple,\r\n material_type: str,\r\n name_prefix: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create floor with material.\"\"\"\r\n bx, by, bz = location\r\n \r\n bpy.ops.mesh.primitive_plane_add(size=1, location=(bx, by, bz))\r\n floor = bpy.context.active_object\r\n floor.name = f\"{name_prefix}_Floor\"\r\n floor.scale = (width/2, depth/2, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Floor material\r\n floor_mat = bpy.data.materials.new(f\"{name_prefix}_FloorMat\")\r\n nodes = floor_mat.node_tree.nodes\r\n links = floor_mat.node_tree.links\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n materials = {\r\n 'WOOD': {'color': (0.4, 0.25, 0.1, 1.0), 'roughness': 0.4},\r\n 'TILE': {'color': (0.8, 0.8, 0.78, 1.0), 'roughness': 0.3},\r\n 'CARPET': {'color': (0.3, 0.3, 0.35, 1.0), 'roughness': 0.95},\r\n 'CONCRETE': {'color': (0.5, 0.5, 0.5, 1.0), 'roughness': 0.8}\r\n }\r\n \r\n mat_props = materials.get(material_type, materials['WOOD'])\r\n bsdf.inputs['Base Color'].default_value = mat_props['color']\r\n bsdf.inputs['Roughness'].default_value = mat_props['roughness']\r\n \r\n floor.data.materials.append(floor_mat)\r\n \r\n return floor\r\n\r\n\r\ndef _create_ceiling(\r\n width: float,\r\n depth: float,\r\n height: float,\r\n location: tuple,\r\n name_prefix: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create ceiling.\"\"\"\r\n bx, by, bz = location\r\n \r\n bpy.ops.mesh.primitive_plane_add(size=1, location=(bx, by, bz + height))\r\n ceiling = bpy.context.active_object\r\n ceiling.name = f\"{name_prefix}_Ceiling\"\r\n ceiling.scale = (width/2, depth/2, 1)\r\n ceiling.rotation_euler[0] = math.radians(180) # Flip normals down\r\n bpy.ops.object.transform_apply(scale=True, rotation=True)\r\n \r\n # Ceiling material\r\n ceiling_mat = bpy.data.materials.new(f\"{name_prefix}_CeilingMat\")\r\n bsdf = ceiling_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.95, 0.95, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n \r\n ceiling.data.materials.append(ceiling_mat)\r\n \r\n return ceiling\r\n\r\n\r\ndef add_door_opening(\r\n wall: bpy.types.Object,\r\n door_width: float = 0.9,\r\n door_height: float = 2.1,\r\n position: float = 0.5,\r\n name: str = \"DoorOpening\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add a door opening to a wall using boolean.\r\n \r\n Args:\r\n wall: Wall object to cut\r\n door_width: Door width\r\n door_height: Door height\r\n position: Horizontal position (0-1 along wall)\r\n name: Cutting object name\r\n \r\n Returns:\r\n The cutting cube (hidden after operation)\r\n \"\"\"\r\n # Create cutting cube\r\n wall_dims = wall.dimensions\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1)\r\n cutter = bpy.context.active_object\r\n cutter.name = name\r\n \r\n # Position at door location\r\n cutter.location = wall.location.copy()\r\n cutter.location.z = door_height / 2\r\n \r\n # Scale to door size\r\n cutter.scale = (door_width / 2, wall_dims.y + 0.1, door_height / 2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Add boolean modifier\r\n bool_mod = wall.modifiers.new(name=f\"Door_{name}\", type='BOOLEAN')\r\n bool_mod.operation = 'DIFFERENCE'\r\n bool_mod.object = cutter\r\n \r\n # Apply modifier\r\n bpy.context.view_layer.objects.active = wall\r\n bpy.ops.object.modifier_apply(modifier=bool_mod.name)\r\n \r\n # Hide cutter\r\n cutter.hide_viewport = True\r\n cutter.hide_render = True\r\n \r\n return cutter\r\n\r\n\r\ndef add_window_opening(\r\n wall: bpy.types.Object,\r\n window_width: float = 1.2,\r\n window_height: float = 1.0,\r\n sill_height: float = 0.9,\r\n position: float = 0.5,\r\n name: str = \"WindowOpening\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add a window opening to a wall.\r\n \r\n Args:\r\n wall: Wall object to cut\r\n window_width: Window width\r\n window_height: Window height\r\n sill_height: Height from floor to window sill\r\n position: Horizontal position (0-1)\r\n name: Object name\r\n \r\n Returns:\r\n The cutting cube\r\n \"\"\"\r\n wall_dims = wall.dimensions\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1)\r\n cutter = bpy.context.active_object\r\n cutter.name = name\r\n \r\n cutter.location = wall.location.copy()\r\n cutter.location.z = sill_height + window_height / 2\r\n \r\n cutter.scale = (window_width / 2, wall_dims.y + 0.1, window_height / 2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n bool_mod = wall.modifiers.new(name=f\"Window_{name}\", type='BOOLEAN')\r\n bool_mod.operation = 'DIFFERENCE'\r\n bool_mod.object = cutter\r\n \r\n bpy.context.view_layer.objects.active = wall\r\n bpy.ops.object.modifier_apply(modifier=bool_mod.name)\r\n \r\n cutter.hide_viewport = True\r\n cutter.hide_render = True\r\n \r\n return cutter\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create a room\r\n room = create_interior_room(\r\n width=5,\r\n depth=4,\r\n height=2.8,\r\n floor_material='WOOD'\r\n )\r\n \r\n # Add door to front wall\r\n if 'front' in room:\r\n add_door_opening(room['front'])\r\n \r\n # Add window to left wall\r\n if 'left' in room:\r\n add_window_opening(room['left'])\r\n \r\n print(f\"Created room with {len(room)} elements\")","category":"tasks/architecture","source_file":"tasks\\architecture\\interior_room.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Procedural Building Generator. Creates a procedural building with customizable floors, windows, and facade details.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_procedural_building(\r\n location: tuple = (0, 0, 0),\r\n width: float = 10.0,\r\n depth: float = 8.0,\r\n floors: int = 5,\r\n floor_height: float = 3.0,\r\n window_rows: int = 4,\r\n window_cols: int = 3,\r\n name_prefix: str = \"Building\"\r\n) -> dict:\r\n \"\"\"\r\n Create a procedural building with windows and details.\r\n \r\n Args:\r\n location: Base position (ground level)\r\n width: Building width (X axis)\r\n depth: Building depth (Y axis)\r\n floors: Number of floors\r\n floor_height: Height per floor in meters\r\n window_rows: Windows per floor (horizontal)\r\n window_cols: Window columns per side\r\n name_prefix: Prefix for object names\r\n \r\n Returns:\r\n Dictionary with building components\r\n \r\n Example:\r\n >>> building = create_procedural_building(floors=8, width=15)\r\n \"\"\"\r\n created = {}\r\n bx, by, bz = location\r\n total_height = floors * floor_height\r\n \r\n # === MAIN BODY ===\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(bx, by, bz + total_height / 2))\r\n body = bpy.context.active_object\r\n body.name = f\"{name_prefix}_Body\"\r\n body.scale = (width / 2, depth / 2, total_height / 2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Building material\r\n building_mat = bpy.data.materials.new(name=f\"{name_prefix}_Facade\")\r\n bsdf = building_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.7, 0.68, 0.65, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n body.data.materials.append(building_mat)\r\n created['body'] = body\r\n \r\n # === WINDOWS ===\r\n glass_mat = bpy.data.materials.new(name=f\"{name_prefix}_Glass\")\r\n glass_bsdf = glass_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n glass_bsdf.inputs['Base Color'].default_value = (0.2, 0.3, 0.4, 1.0)\r\n glass_bsdf.inputs['Metallic'].default_value = 0.8\r\n glass_bsdf.inputs['Roughness'].default_value = 0.1\r\n \r\n windows = []\r\n window_width = (width * 0.8) / window_rows\r\n window_height = floor_height * 0.5\r\n \r\n for floor in range(floors):\r\n floor_z = bz + (floor * floor_height) + floor_height * 0.6\r\n \r\n for row in range(window_rows):\r\n window_x = bx - (width * 0.4) + (row + 0.5) * window_width\r\n \r\n # Front windows\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1,\r\n location=(window_x, by + depth / 2 + 0.01, floor_z)\r\n )\r\n win = bpy.context.active_object\r\n win.name = f\"{name_prefix}_Window_F{floor}_R{row}\"\r\n win.scale = (window_width * 0.7 / 2, 0.05, window_height / 2)\r\n win.data.materials.append(glass_mat)\r\n windows.append(win)\r\n \r\n # Back windows\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1,\r\n location=(window_x, by - depth / 2 - 0.01, floor_z)\r\n )\r\n win = bpy.context.active_object\r\n win.name = f\"{name_prefix}_Window_B{floor}_R{row}\"\r\n win.scale = (window_width * 0.7 / 2, 0.05, window_height / 2)\r\n win.data.materials.append(glass_mat)\r\n windows.append(win)\r\n \r\n created['windows'] = windows\r\n \r\n # === ROOF DETAILS ===\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1,\r\n location=(bx, by, bz + total_height + 0.3)\r\n )\r\n roof_edge = bpy.context.active_object\r\n roof_edge.name = f\"{name_prefix}_RoofEdge\"\r\n roof_edge.scale = (width / 2 + 0.2, depth / 2 + 0.2, 0.3)\r\n \r\n roof_mat = bpy.data.materials.new(name=f\"{name_prefix}_Concrete\")\r\n roof_bsdf = roof_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n roof_bsdf.inputs['Base Color'].default_value = (0.4, 0.4, 0.4, 1.0)\r\n roof_bsdf.inputs['Roughness'].default_value = 0.9\r\n roof_edge.data.materials.append(roof_mat)\r\n created['roof'] = roof_edge\r\n \r\n # === ENTRANCE ===\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1,\r\n location=(bx, by + depth / 2 + 0.5, bz + 1.5)\r\n )\r\n entrance = bpy.context.active_object\r\n entrance.name = f\"{name_prefix}_Entrance\"\r\n entrance.scale = (2.0, 0.5, 1.5)\r\n entrance.data.materials.append(glass_mat)\r\n created['entrance'] = entrance\r\n \r\n return created\r\n\r\n\r\ndef create_window_array(\r\n wall_object: bpy.types.Object,\r\n rows: int = 3,\r\n cols: int = 2,\r\n window_size: tuple = (1.0, 1.5),\r\n spacing: tuple = (0.5, 0.3)\r\n) -> list:\r\n \"\"\"\r\n Create an array of windows on a wall surface.\r\n \r\n Args:\r\n wall_object: The wall to add windows to\r\n rows: Number of horizontal windows\r\n cols: Number of vertical rows\r\n window_size: (width, height) of each window\r\n spacing: (horizontal, vertical) spacing between windows\r\n \r\n Returns:\r\n List of created window objects\r\n \"\"\"\r\n windows = []\r\n wall_dims = wall_object.dimensions\r\n wall_loc = wall_object.location\r\n \r\n total_width = rows * (window_size[0] + spacing[0]) - spacing[0]\r\n total_height = cols * (window_size[1] + spacing[1]) - spacing[1]\r\n \r\n start_x = wall_loc.x - total_width / 2 + window_size[0] / 2\r\n start_z = wall_loc.z - total_height / 2 + window_size[1] / 2\r\n \r\n for row in range(rows):\r\n for col in range(cols):\r\n x = start_x + row * (window_size[0] + spacing[0])\r\n z = start_z + col * (window_size[1] + spacing[1])\r\n \r\n bpy.ops.mesh.primitive_plane_add(\r\n size=1,\r\n location=(x, wall_loc.y + wall_dims.y / 2 + 0.01, z)\r\n )\r\n window = bpy.context.active_object\r\n window.scale = (window_size[0] / 2, 1, window_size[1] / 2)\r\n window.rotation_euler[0] = math.radians(90)\r\n windows.append(window)\r\n \r\n return windows\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n building = create_procedural_building(\r\n location=(0, 0, 0),\r\n floors=6,\r\n width=12,\r\n depth=10,\r\n window_rows=5\r\n )\r\n print(f\"Created building with {len(building)} component groups\")","category":"tasks/architecture","source_file":"tasks\\architecture\\procedural_building.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Humanoid Armature Generator. Creates a basic humanoid armature/skeleton suitable for character animation.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_humanoid_armature(\r\n location: tuple = (0, 0, 0),\r\n height: float = 1.8,\r\n name: str = \"Humanoid\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a basic humanoid armature with proper bone hierarchy.\r\n \r\n The armature includes:\r\n - Spine chain (hips, spine, chest, neck, head)\r\n - Arms (shoulder, upper_arm, forearm, hand)\r\n - Legs (thigh, shin, foot, toe)\r\n \r\n Args:\r\n location: Base position (feet level)\r\n height: Total height in meters\r\n name: Armature name\r\n \r\n Returns:\r\n The created armature object\r\n \r\n Example:\r\n >>> armature = create_humanoid_armature(height=1.75, name=\"Hero\")\r\n \"\"\"\r\n # Proportions (relative to height)\r\n scale = height / 1.8\r\n \r\n # Create armature\r\n bpy.ops.object.armature_add(location=location)\r\n armature = bpy.context.active_object\r\n armature.name = name\r\n armature.show_in_front = True\r\n \r\n # Enter edit mode\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n arm_data = armature.data\r\n \r\n # Remove default bone\r\n for bone in arm_data.edit_bones:\r\n arm_data.edit_bones.remove(bone)\r\n \r\n # Bone positions (relative to base, scaled)\r\n def pos(x, y, z):\r\n return (x * scale, y * scale, z * scale + location[2])\r\n \r\n # === SPINE ===\r\n hips = arm_data.edit_bones.new(\"Hips\")\r\n hips.head = pos(0, 0, 0.95)\r\n hips.tail = pos(0, 0, 1.05)\r\n \r\n spine = arm_data.edit_bones.new(\"Spine\")\r\n spine.head = pos(0, 0, 1.05)\r\n spine.tail = pos(0, 0, 1.25)\r\n spine.parent = hips\r\n \r\n chest = arm_data.edit_bones.new(\"Chest\")\r\n chest.head = pos(0, 0, 1.25)\r\n chest.tail = pos(0, 0, 1.45)\r\n chest.parent = spine\r\n \r\n neck = arm_data.edit_bones.new(\"Neck\")\r\n neck.head = pos(0, 0, 1.45)\r\n neck.tail = pos(0, 0, 1.55)\r\n neck.parent = chest\r\n \r\n head = arm_data.edit_bones.new(\"Head\")\r\n head.head = pos(0, 0, 1.55)\r\n head.tail = pos(0, 0, 1.8)\r\n head.parent = neck\r\n \r\n # === LEFT ARM ===\r\n shoulder_l = arm_data.edit_bones.new(\"Shoulder.L\")\r\n shoulder_l.head = pos(0, 0, 1.42)\r\n shoulder_l.tail = pos(0.15, 0, 1.42)\r\n shoulder_l.parent = chest\r\n \r\n upper_arm_l = arm_data.edit_bones.new(\"UpperArm.L\")\r\n upper_arm_l.head = pos(0.15, 0, 1.42)\r\n upper_arm_l.tail = pos(0.42, 0, 1.42)\r\n upper_arm_l.parent = shoulder_l\r\n upper_arm_l.use_connect = True\r\n \r\n forearm_l = arm_data.edit_bones.new(\"Forearm.L\")\r\n forearm_l.head = pos(0.42, 0, 1.42)\r\n forearm_l.tail = pos(0.67, 0, 1.42)\r\n forearm_l.parent = upper_arm_l\r\n forearm_l.use_connect = True\r\n \r\n hand_l = arm_data.edit_bones.new(\"Hand.L\")\r\n hand_l.head = pos(0.67, 0, 1.42)\r\n hand_l.tail = pos(0.77, 0, 1.42)\r\n hand_l.parent = forearm_l\r\n hand_l.use_connect = True\r\n \r\n # === RIGHT ARM ===\r\n shoulder_r = arm_data.edit_bones.new(\"Shoulder.R\")\r\n shoulder_r.head = pos(0, 0, 1.42)\r\n shoulder_r.tail = pos(-0.15, 0, 1.42)\r\n shoulder_r.parent = chest\r\n \r\n upper_arm_r = arm_data.edit_bones.new(\"UpperArm.R\")\r\n upper_arm_r.head = pos(-0.15, 0, 1.42)\r\n upper_arm_r.tail = pos(-0.42, 0, 1.42)\r\n upper_arm_r.parent = shoulder_r\r\n upper_arm_r.use_connect = True\r\n \r\n forearm_r = arm_data.edit_bones.new(\"Forearm.R\")\r\n forearm_r.head = pos(-0.42, 0, 1.42)\r\n forearm_r.tail = pos(-0.67, 0, 1.42)\r\n forearm_r.parent = upper_arm_r\r\n forearm_r.use_connect = True\r\n \r\n hand_r = arm_data.edit_bones.new(\"Hand.R\")\r\n hand_r.head = pos(-0.67, 0, 1.42)\r\n hand_r.tail = pos(-0.77, 0, 1.42)\r\n hand_r.parent = forearm_r\r\n hand_r.use_connect = True\r\n \r\n # === LEFT LEG ===\r\n thigh_l = arm_data.edit_bones.new(\"Thigh.L\")\r\n thigh_l.head = pos(0.1, 0, 0.95)\r\n thigh_l.tail = pos(0.1, 0, 0.5)\r\n thigh_l.parent = hips\r\n \r\n shin_l = arm_data.edit_bones.new(\"Shin.L\")\r\n shin_l.head = pos(0.1, 0, 0.5)\r\n shin_l.tail = pos(0.1, 0, 0.08)\r\n shin_l.parent = thigh_l\r\n shin_l.use_connect = True\r\n \r\n foot_l = arm_data.edit_bones.new(\"Foot.L\")\r\n foot_l.head = pos(0.1, 0, 0.08)\r\n foot_l.tail = pos(0.1, -0.12, 0)\r\n foot_l.parent = shin_l\r\n foot_l.use_connect = True\r\n \r\n toe_l = arm_data.edit_bones.new(\"Toe.L\")\r\n toe_l.head = pos(0.1, -0.12, 0)\r\n toe_l.tail = pos(0.1, -0.20, 0)\r\n toe_l.parent = foot_l\r\n toe_l.use_connect = True\r\n \r\n # === RIGHT LEG ===\r\n thigh_r = arm_data.edit_bones.new(\"Thigh.R\")\r\n thigh_r.head = pos(-0.1, 0, 0.95)\r\n thigh_r.tail = pos(-0.1, 0, 0.5)\r\n thigh_r.parent = hips\r\n \r\n shin_r = arm_data.edit_bones.new(\"Shin.R\")\r\n shin_r.head = pos(-0.1, 0, 0.5)\r\n shin_r.tail = pos(-0.1, 0, 0.08)\r\n shin_r.parent = thigh_r\r\n shin_r.use_connect = True\r\n \r\n foot_r = arm_data.edit_bones.new(\"Foot.R\")\r\n foot_r.head = pos(-0.1, 0, 0.08)\r\n foot_r.tail = pos(-0.1, -0.12, 0)\r\n foot_r.parent = shin_r\r\n foot_r.use_connect = True\r\n \r\n toe_r = arm_data.edit_bones.new(\"Toe.R\")\r\n toe_r.head = pos(-0.1, -0.12, 0)\r\n toe_r.tail = pos(-0.1, -0.20, 0)\r\n toe_r.parent = foot_r\r\n toe_r.use_connect = True\r\n \r\n # Return to object mode\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n return armature\r\n\r\n\r\ndef parent_mesh_to_armature(\r\n mesh_object: bpy.types.Object,\r\n armature_object: bpy.types.Object,\r\n parenting_type: str = 'AUTOMATIC'\r\n) -> None:\r\n \"\"\"\r\n Parent a mesh to an armature with automatic or manual weights.\r\n \r\n Args:\r\n mesh_object: The mesh to parent\r\n armature_object: The target armature\r\n parenting_type: 'AUTOMATIC', 'ENVELOPE', or 'EMPTY' (manual weight painting)\r\n \r\n Example:\r\n >>> parent_mesh_to_armature(body_mesh, humanoid_rig, 'AUTOMATIC')\r\n \"\"\"\r\n # Deselect all\r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n # Select mesh first, then armature (armature must be active)\r\n mesh_object.select_set(True)\r\n armature_object.select_set(True)\r\n bpy.context.view_layer.objects.active = armature_object\r\n \r\n # Parent with weights\r\n if parenting_type == 'AUTOMATIC':\r\n bpy.ops.object.parent_set(type='ARMATURE_AUTO')\r\n elif parenting_type == 'ENVELOPE':\r\n bpy.ops.object.parent_set(type='ARMATURE_ENVELOPE')\r\n else:\r\n bpy.ops.object.parent_set(type='ARMATURE')\r\n\r\n\r\ndef add_ik_constraint(\r\n armature: bpy.types.Object,\r\n bone_name: str,\r\n target_bone: str = None,\r\n chain_length: int = 2,\r\n pole_bone: str = None\r\n) -> bpy.types.Constraint:\r\n \"\"\"\r\n Add an Inverse Kinematics constraint to a bone.\r\n \r\n Args:\r\n armature: The armature object\r\n bone_name: Bone to add IK to (e.g., 'Forearm.L')\r\n target_bone: Target bone name (creates if None)\r\n chain_length: Number of bones in IK chain\r\n pole_bone: Pole target bone name (optional)\r\n \r\n Returns:\r\n The created IK constraint\r\n \r\n Example:\r\n >>> add_ik_constraint(rig, 'Forearm.L', chain_length=2)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = armature\r\n bpy.ops.object.mode_set(mode='POSE')\r\n \r\n pose_bone = armature.pose.bones.get(bone_name)\r\n if not pose_bone:\r\n raise ValueError(f\"Bone '{bone_name}' not found\")\r\n \r\n # Add IK constraint\r\n ik = pose_bone.constraints.new('IK')\r\n ik.chain_count = chain_length\r\n \r\n if target_bone:\r\n ik.target = armature\r\n ik.subtarget = target_bone\r\n \r\n if pole_bone:\r\n ik.pole_target = armature\r\n ik.pole_subtarget = pole_bone\r\n ik.pole_angle = math.radians(90)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n return ik\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create humanoid armature\r\n armature = create_humanoid_armature(height=1.8, name=\"Character\")\r\n \r\n print(f\"Created armature with {len(armature.data.bones)} bones\")","category":"tasks/characters","source_file":"tasks\\characters\\humanoid_armature.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Simple Character Body. Creates a simple stylized humanoid character mesh suitable for animation.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_simple_character(\r\n height: float = 1.8,\r\n style: str = 'CAPSULE',\r\n proportions: str = 'REALISTIC',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Character\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a simple humanoid character.\r\n \r\n Args:\r\n height: Character height\r\n style: 'CAPSULE' (pill-shaped), 'BLOCKY', 'ROUND'\r\n proportions: 'REALISTIC', 'CHIBI', 'HEROIC'\r\n location: Character base position\r\n name: Object name\r\n \r\n Returns:\r\n The created character mesh\r\n \r\n Example:\r\n >>> char = create_simple_character(height=2.0, style='BLOCKY')\r\n \"\"\"\r\n # Proportion presets\r\n props = {\r\n 'REALISTIC': {\r\n 'head_ratio': 0.13,\r\n 'torso_ratio': 0.30,\r\n 'leg_ratio': 0.45,\r\n 'shoulder_width': 0.25,\r\n 'hip_width': 0.22\r\n },\r\n 'CHIBI': {\r\n 'head_ratio': 0.35,\r\n 'torso_ratio': 0.25,\r\n 'leg_ratio': 0.25,\r\n 'shoulder_width': 0.30,\r\n 'hip_width': 0.25\r\n },\r\n 'HEROIC': {\r\n 'head_ratio': 0.11,\r\n 'torso_ratio': 0.35,\r\n 'leg_ratio': 0.45,\r\n 'shoulder_width': 0.35,\r\n 'hip_width': 0.20\r\n }\r\n }\r\n \r\n p = props.get(proportions, props['REALISTIC'])\r\n \r\n # Calculate sizes\r\n head_h = height * p['head_ratio']\r\n torso_h = height * p['torso_ratio']\r\n leg_h = height * p['leg_ratio']\r\n arm_h = height * 0.35\r\n \r\n # Join all parts\r\n parts = []\r\n \r\n # === HEAD ===\r\n head_z = location[2] + height - head_h / 2\r\n \r\n if style == 'CAPSULE':\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=head_h * 0.5,\r\n location=(location[0], location[1], head_z)\r\n )\r\n elif style == 'BLOCKY':\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=head_h,\r\n location=(location[0], location[1], head_z)\r\n )\r\n else: # ROUND\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=head_h * 0.5,\r\n subdivisions=2,\r\n location=(location[0], location[1], head_z)\r\n )\r\n \r\n head = bpy.context.active_object\r\n head.name = f\"{name}_Head\"\r\n parts.append(head)\r\n \r\n # === TORSO ===\r\n torso_z = location[2] + leg_h + torso_h / 2\r\n \r\n if style == 'CAPSULE':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=p['shoulder_width'] / 2 * height,\r\n depth=torso_h,\r\n location=(location[0], location[1], torso_z)\r\n )\r\n torso = bpy.context.active_object\r\n # Taper toward hips\r\n torso.scale.y = 0.5\r\n bpy.ops.object.transform_apply(scale=True)\r\n elif style == 'BLOCKY':\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1,\r\n location=(location[0], location[1], torso_z)\r\n )\r\n torso = bpy.context.active_object\r\n torso.scale = (\r\n p['shoulder_width'] * height / 2,\r\n height * 0.15 / 2,\r\n torso_h / 2\r\n )\r\n bpy.ops.object.transform_apply(scale=True)\r\n else: # ROUND\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=p['shoulder_width'] / 2 * height,\r\n location=(location[0], location[1], torso_z)\r\n )\r\n torso = bpy.context.active_object\r\n torso.scale = (1, 0.7, torso_h / (p['shoulder_width'] * height))\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n torso.name = f\"{name}_Torso\"\r\n parts.append(torso)\r\n \r\n # === LEGS ===\r\n leg_radius = height * 0.05\r\n leg_offset = p['hip_width'] * height / 2 * 0.5\r\n \r\n for side, offset in [('L', -leg_offset), ('R', leg_offset)]:\r\n if style == 'CAPSULE':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=leg_radius * 1.5,\r\n depth=leg_h,\r\n location=(\r\n location[0] + offset,\r\n location[1],\r\n location[2] + leg_h / 2\r\n )\r\n )\r\n elif style == 'BLOCKY':\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1,\r\n location=(\r\n location[0] + offset,\r\n location[1],\r\n location[2] + leg_h / 2\r\n )\r\n )\r\n bpy.context.active_object.scale = (\r\n leg_radius * 1.5,\r\n leg_radius * 1.5,\r\n leg_h / 2\r\n )\r\n bpy.ops.object.transform_apply(scale=True)\r\n else: # ROUND\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=leg_radius * 2,\r\n location=(\r\n location[0] + offset,\r\n location[1],\r\n location[2] + leg_h / 2\r\n )\r\n )\r\n bpy.context.active_object.scale = (1, 1, leg_h / (leg_radius * 4))\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n leg = bpy.context.active_object\r\n leg.name = f\"{name}_Leg_{side}\"\r\n parts.append(leg)\r\n \r\n # === ARMS ===\r\n arm_radius = height * 0.04\r\n arm_z = location[2] + leg_h + torso_h * 0.85\r\n arm_offset = p['shoulder_width'] * height / 2\r\n \r\n for side, offset in [('L', -arm_offset - arm_h/2), ('R', arm_offset + arm_h/2)]:\r\n if style == 'CAPSULE':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=arm_radius * 1.2,\r\n depth=arm_h,\r\n location=(\r\n location[0] + offset,\r\n location[1],\r\n arm_z\r\n )\r\n )\r\n elif style == 'BLOCKY':\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1,\r\n location=(\r\n location[0] + offset,\r\n location[1],\r\n arm_z\r\n )\r\n )\r\n bpy.context.active_object.scale = (\r\n arm_h / 2,\r\n arm_radius * 1.2,\r\n arm_radius * 1.2\r\n )\r\n bpy.ops.object.transform_apply(scale=True)\r\n else: # ROUND\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=arm_radius * 1.5,\r\n location=(\r\n location[0] + offset,\r\n location[1],\r\n arm_z\r\n )\r\n )\r\n bpy.context.active_object.scale = (arm_h / (arm_radius * 3), 1, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n arm = bpy.context.active_object\r\n arm.rotation_euler.y = math.radians(90) if side == 'L' else math.radians(-90)\r\n bpy.ops.object.transform_apply(rotation=True)\r\n arm.name = f\"{name}_Arm_{side}\"\r\n parts.append(arm)\r\n \r\n # Join all parts\r\n bpy.ops.object.select_all(action='DESELECT')\r\n for part in parts:\r\n part.select_set(True)\r\n bpy.context.view_layer.objects.active = parts[0]\r\n bpy.ops.object.join()\r\n \r\n character = bpy.context.active_object\r\n character.name = name\r\n \r\n # Apply smooth shading for non-blocky styles\r\n if style != 'BLOCKY':\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Create simple material\r\n mat = bpy.data.materials.new(f\"{name}_Material\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.6, 0.5, 1.0) # Skin tone\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n character.data.materials.append(mat)\r\n \r\n # Set origin to base\r\n bpy.context.scene.cursor.location = location\r\n bpy.ops.object.origin_set(type='ORIGIN_CURSOR')\r\n \r\n return character\r\n\r\n\r\ndef add_face_features(\r\n character: bpy.types.Object,\r\n eye_size: float = 0.03,\r\n add_mouth: bool = True\r\n) -> dict:\r\n \"\"\"\r\n Add simple face features to character head.\r\n \r\n Args:\r\n character: Character object\r\n eye_size: Size of eyes\r\n add_mouth: Whether to add a mouth\r\n \r\n Returns:\r\n Dictionary of created feature objects\r\n \"\"\"\r\n result = {}\r\n \r\n # Find approximate head position\r\n # (assumes head is at top of character)\r\n bounds = character.bound_box\r\n top_z = max(v[2] for v in bounds)\r\n head_center = (0, 0, top_z - 0.1)\r\n \r\n # Eyes\r\n for side, offset in [('L', -eye_size * 2), ('R', eye_size * 2)]:\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=eye_size,\r\n location=(\r\n offset,\r\n -0.12, # Forward\r\n head_center[2] + 0.02\r\n )\r\n )\r\n eye = bpy.context.active_object\r\n eye.name = f\"Eye_{side}\"\r\n \r\n # Eye material (white with black pupil effect)\r\n mat = bpy.data.materials.new(f\"EyeMat_{side}\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.1, 0.1, 0.1, 1.0) # Dark pupil\r\n eye.data.materials.append(mat)\r\n \r\n result[f'eye_{side.lower()}'] = eye\r\n \r\n if add_mouth:\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=1,\r\n location=(0, -0.12, head_center[2] - 0.05)\r\n )\r\n mouth = bpy.context.active_object\r\n mouth.name = \"Mouth\"\r\n mouth.scale = (eye_size * 2, eye_size * 0.3, eye_size * 0.5)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n mat = bpy.data.materials.new(\"MouthMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.1, 0.1, 1.0)\r\n mouth.data.materials.append(mat)\r\n \r\n result['mouth'] = mouth\r\n \r\n return result\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create different character styles\r\n char1 = create_simple_character(\r\n height=1.8,\r\n style='CAPSULE',\r\n proportions='REALISTIC',\r\n location=(0, 0, 0),\r\n name=\"RealisticChar\"\r\n )\r\n \r\n char2 = create_simple_character(\r\n height=1.0,\r\n style='ROUND',\r\n proportions='CHIBI',\r\n location=(3, 0, 0),\r\n name=\"ChibiChar\"\r\n )\r\n \r\n char3 = create_simple_character(\r\n height=2.2,\r\n style='BLOCKY',\r\n proportions='HEROIC',\r\n location=(-3, 0, 0),\r\n name=\"HeroChar\"\r\n )\r\n \r\n print(\"Created 3 character variations\")","category":"tasks/characters","source_file":"tasks\\characters\\simple_character.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Particle Explosion Effect. Creates an explosion particle effect with debris, smoke, and fire elements.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_explosion_effect(\r\n location: tuple = (0, 0, 0),\r\n intensity: float = 1.0,\r\n duration: int = 60,\r\n start_frame: int = 1,\r\n include_debris: bool = True,\r\n include_smoke: bool = True,\r\n include_fire: bool = True,\r\n name: str = \"Explosion\"\r\n) -> dict:\r\n \"\"\"\r\n Create a complete explosion particle effect.\r\n \r\n Args:\r\n location: Explosion center\r\n intensity: Scale multiplier for the effect\r\n duration: Effect duration in frames\r\n start_frame: Frame when explosion begins\r\n include_debris: Add debris particles\r\n include_smoke: Add smoke simulation\r\n include_fire: Add fire/glow elements\r\n name: Prefix for created objects\r\n \r\n Returns:\r\n Dictionary with all created objects and systems\r\n \r\n Example:\r\n >>> explosion = create_explosion_effect((0, 0, 1), intensity=1.5)\r\n \"\"\"\r\n result = {}\r\n \r\n # Create emitter sphere\r\n bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.5 * intensity, location=location)\r\n emitter = bpy.context.active_object\r\n emitter.name = f\"{name}_Emitter\"\r\n emitter.hide_render = True\r\n result['emitter'] = emitter\r\n \r\n if include_debris:\r\n # Create debris particle system\r\n debris_system = _create_debris_particles(emitter, intensity, duration, start_frame, name)\r\n result['debris'] = debris_system\r\n \r\n if include_smoke:\r\n # Create smoke domain and emitter\r\n smoke_result = _create_smoke_effect(location, intensity, duration, start_frame, name)\r\n result['smoke'] = smoke_result\r\n \r\n if include_fire:\r\n # Create fire/glow sphere\r\n fire_result = _create_fire_glow(location, intensity, duration, start_frame, name)\r\n result['fire'] = fire_result\r\n \r\n # Set frame range\r\n bpy.context.scene.frame_start = start_frame\r\n bpy.context.scene.frame_end = start_frame + duration\r\n \r\n return result\r\n\r\n\r\ndef _create_debris_particles(\r\n emitter: bpy.types.Object,\r\n intensity: float,\r\n duration: int,\r\n start_frame: int,\r\n name: str\r\n) -> bpy.types.ParticleSystem:\r\n \"\"\"Create debris particle system.\"\"\"\r\n # Create debris object\r\n bpy.ops.mesh.primitive_cube_add(size=0.1 * intensity)\r\n debris_obj = bpy.context.active_object\r\n debris_obj.name = f\"{name}_DebrisObject\"\r\n debris_obj.hide_viewport = True\r\n debris_obj.hide_render = True\r\n \r\n # Add material\r\n debris_mat = bpy.data.materials.new(name=f\"{name}_DebrisMat\")\r\n bsdf = debris_mat.node_tree.nodes[\"Principled BSDF\"]\r\n bsdf.inputs['Base Color'].default_value = (0.15, 0.12, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n debris_obj.data.materials.append(debris_mat)\r\n \r\n # Add particle system to emitter\r\n bpy.context.view_layer.objects.active = emitter\r\n bpy.ops.object.particle_system_add()\r\n particle_sys = emitter.particle_systems[-1]\r\n particle_sys.name = f\"{name}_Debris\"\r\n settings = particle_sys.settings\r\n \r\n # Configure as emission particles\r\n settings.type = 'EMITTER'\r\n settings.count = int(100 * intensity)\r\n settings.frame_start = start_frame\r\n settings.frame_end = start_frame + 5 # Quick burst\r\n settings.lifetime = duration - 5\r\n settings.lifetime_random = 0.2\r\n \r\n # Physics\r\n settings.physics_type = 'NEWTON'\r\n settings.mass = 1.0\r\n settings.normal_factor = 10.0 * intensity\r\n settings.factor_random = 0.5\r\n settings.drag_factor = 0.1\r\n \r\n # Gravity\r\n settings.effector_weights.gravity = 1.0\r\n \r\n # Rotation\r\n settings.use_rotations = True\r\n settings.rotation_mode = 'VEL'\r\n settings.angular_velocity_mode = 'RAND'\r\n settings.angular_velocity_factor = 5.0\r\n \r\n # Render as object\r\n settings.render_type = 'OBJECT'\r\n settings.instance_object = debris_obj\r\n settings.particle_size = 1.0\r\n settings.size_random = 0.8\r\n \r\n return particle_sys\r\n\r\n\r\ndef _create_smoke_effect(\r\n location: tuple,\r\n intensity: float,\r\n duration: int,\r\n start_frame: int,\r\n name: str\r\n) -> dict:\r\n \"\"\"Create smoke simulation domain and flow.\"\"\"\r\n result = {}\r\n \r\n # Create domain\r\n domain_size = 5.0 * intensity\r\n bpy.ops.mesh.primitive_cube_add(size=domain_size, location=(location[0], location[1], location[2] + domain_size / 2))\r\n domain = bpy.context.active_object\r\n domain.name = f\"{name}_SmokeDomain\"\r\n \r\n # Add fluid modifier (domain)\r\n bpy.ops.object.modifier_add(type='FLUID')\r\n domain.modifiers[\"Fluid\"].fluid_type = 'DOMAIN'\r\n domain_settings = domain.modifiers[\"Fluid\"].domain_settings\r\n domain_settings.domain_type = 'GAS'\r\n domain_settings.resolution_max = 64\r\n domain_settings.use_noise = True\r\n domain_settings.noise_scale = 2\r\n \r\n # Cache settings\r\n domain_settings.cache_frame_start = start_frame\r\n domain_settings.cache_frame_end = start_frame + duration\r\n \r\n result['domain'] = domain\r\n \r\n # Create flow emitter\r\n bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.3 * intensity, location=location)\r\n flow = bpy.context.active_object\r\n flow.name = f\"{name}_SmokeFlow\"\r\n \r\n # Add fluid modifier (flow)\r\n bpy.ops.object.modifier_add(type='FLUID')\r\n flow.modifiers[\"Fluid\"].fluid_type = 'FLOW'\r\n flow_settings = flow.modifiers[\"Fluid\"].flow_settings\r\n flow_settings.flow_type = 'SMOKE'\r\n flow_settings.flow_behavior = 'INFLOW'\r\n flow_settings.use_initial_velocity = True\r\n flow_settings.velocity_factor = 5.0 * intensity\r\n \r\n # Animate flow\r\n flow_settings.keyframe_insert('flow', frame=start_frame)\r\n flow_settings.flow = True\r\n flow_settings.keyframe_insert('flow', frame=start_frame)\r\n flow_settings.flow = False\r\n flow_settings.keyframe_insert('flow', frame=start_frame + 10)\r\n \r\n result['flow'] = flow\r\n \r\n return result\r\n\r\n\r\ndef _create_fire_glow(\r\n location: tuple,\r\n intensity: float,\r\n duration: int,\r\n start_frame: int,\r\n name: str\r\n) -> dict:\r\n \"\"\"Create expanding fire/glow sphere.\"\"\"\r\n result = {}\r\n \r\n # Create sphere\r\n bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=3, radius=0.1, location=location)\r\n glow = bpy.context.active_object\r\n glow.name = f\"{name}_FireGlow\"\r\n \r\n # Emission material\r\n glow_mat = bpy.data.materials.new(name=f\"{name}_FireMat\")\r\n nodes = glow_mat.node_tree.nodes\r\n links = glow_mat.node_tree.links\r\n nodes.clear()\r\n \r\n # Create emission shader\r\n emission = nodes.new('ShaderNodeEmission')\r\n emission.inputs['Color'].default_value = (1.0, 0.3, 0.05, 1.0)\r\n emission.inputs['Strength'].default_value = 50.0 * intensity\r\n \r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n links.new(emission.outputs['Emission'], output.inputs['Surface'])\r\n \r\n glow.data.materials.append(glow_mat)\r\n result['object'] = glow\r\n \r\n # Animate scale (expand and fade)\r\n bpy.context.scene.frame_set(start_frame)\r\n glow.scale = (0.1, 0.1, 0.1)\r\n glow.keyframe_insert('scale', frame=start_frame)\r\n emission.inputs['Strength'].keyframe_insert('default_value', frame=start_frame)\r\n \r\n bpy.context.scene.frame_set(start_frame + 10)\r\n glow.scale = (2.0 * intensity, 2.0 * intensity, 2.0 * intensity)\r\n glow.keyframe_insert('scale', frame=start_frame + 10)\r\n \r\n emission.inputs['Strength'].default_value = 0.0\r\n emission.inputs['Strength'].keyframe_insert('default_value', frame=start_frame + 30)\r\n \r\n result['material'] = glow_mat\r\n \r\n return result\r\n\r\n\r\ndef create_shatter_effect(\r\n target_object: bpy.types.Object,\r\n shards: int = 20,\r\n explosion_force: float = 5.0,\r\n start_frame: int = 1\r\n) -> list:\r\n \"\"\"\r\n Shatter an object into pieces with physics simulation.\r\n \r\n Args:\r\n target_object: Object to shatter\r\n shards: Number of pieces\r\n explosion_force: Initial explosion velocity\r\n start_frame: Frame when shatter begins\r\n \r\n Returns:\r\n List of shard objects\r\n \r\n Example:\r\n >>> shards = create_shatter_effect(glass_object, shards=30)\r\n \"\"\"\r\n # Add cell fracture modifier (requires addon)\r\n try:\r\n bpy.ops.object.add_fracture_cell_objects(\r\n source={'PARTICLE_OWN'},\r\n source_limit=shards,\r\n source_noise=0.1,\r\n cell_scale=(1, 1, 1),\r\n recursion=0,\r\n recursion_chance=0.5\r\n )\r\n except:\r\n # Fallback: simple subdivision approach\r\n print(\"Cell Fracture addon not available, using basic approach\")\r\n return []\r\n \r\n # Get created shards\r\n shards_list = [obj for obj in bpy.context.selected_objects if obj != target_object]\r\n \r\n # Add rigid body physics to each shard\r\n for shard in shards_list:\r\n bpy.context.view_layer.objects.active = shard\r\n bpy.ops.rigidbody.object_add(type='ACTIVE')\r\n shard.rigid_body.mass = 1.0 / len(shards_list)\r\n \r\n # Add initial velocity (outward from center)\r\n center = target_object.location\r\n direction = (shard.location - center).normalized()\r\n # Note: Initial velocity would be set via force field or animation\r\n \r\n # Hide original\r\n target_object.hide_viewport = True\r\n target_object.hide_render = True\r\n \r\n return shards_list\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create simple explosion\r\n explosion = create_explosion_effect(\r\n location=(0, 0, 1),\r\n intensity=1.0,\r\n duration=60,\r\n include_debris=True,\r\n include_smoke=False, # Skip smoke for faster testing\r\n include_fire=True\r\n )\r\n \r\n print(f\"Created explosion with {len(explosion)} components\")","category":"tasks/effects","source_file":"tasks\\effects\\explosion_particles.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Glow and Bloom Effect. Creates glowing objects with bloom compositing setup.","output":"import bpy\r\n\r\n\r\ndef create_glow_material(\r\n name: str = \"GlowMat\",\r\n color: tuple = (0, 0.8, 1),\r\n emission_strength: float = 10.0,\r\n base_color: tuple = None\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Create a glowing material.\r\n \r\n Args:\r\n name: Material name\r\n color: RGB glow color\r\n emission_strength: Emission intensity\r\n base_color: Optional base color (uses glow color if None)\r\n \r\n Returns:\r\n The glowing material\r\n \"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*(base_color or color), 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = emission_strength\r\n bsdf.inputs['Roughness'].default_value = 0.3\r\n \r\n return mat\r\n\r\n\r\ndef create_neon_tube(\r\n length: float = 1.0,\r\n radius: float = 0.02,\r\n color: tuple = (1, 0, 0.5),\r\n intensity: float = 20.0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"NeonTube\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a neon tube light.\r\n \r\n Args:\r\n length: Tube length\r\n radius: Tube radius\r\n color: RGB glow color\r\n intensity: Emission strength\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The neon tube object\r\n \"\"\"\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius,\r\n depth=length,\r\n location=location\r\n )\r\n tube = bpy.context.active_object\r\n tube.name = name\r\n \r\n mat = create_glow_material(f\"{name}_Mat\", color, intensity)\r\n tube.data.materials.append(mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n return tube\r\n\r\n\r\ndef setup_bloom_compositing(\r\n threshold: float = 0.8,\r\n intensity: float = 1.0,\r\n size: int = 8\r\n) -> dict:\r\n \"\"\"\r\n Set up bloom/glow compositing.\r\n \r\n Args:\r\n threshold: Brightness threshold for glow\r\n intensity: Glow intensity\r\n size: Glow size\r\n \r\n Returns:\r\n Dictionary with compositing nodes\r\n \"\"\"\r\n tree = bpy.context.scene.node_tree\r\n nodes = tree.nodes\r\n links = tree.links\r\n \r\n nodes.clear()\r\n \r\n # Render layers\r\n render = nodes.new('CompositorNodeRLayers')\r\n render.location = (0, 0)\r\n \r\n # Glare for bloom\r\n glare = nodes.new('CompositorNodeGlare')\r\n glare.location = (200, 0)\r\n glare.glare_type = 'FOG_GLOW'\r\n glare.threshold = threshold\r\n glare.size = size\r\n glare.mix = intensity * -0.9 # Negative = additive\r\n \r\n # Color correction\r\n color = nodes.new('CompositorNodeColorCorrection')\r\n color.location = (400, 0)\r\n \r\n # Output\r\n composite = nodes.new('CompositorNodeComposite')\r\n composite.location = (600, 0)\r\n \r\n viewer = nodes.new('CompositorNodeViewer')\r\n viewer.location = (600, -200)\r\n \r\n # Connect\r\n links.new(render.outputs['Image'], glare.inputs['Image'])\r\n links.new(glare.outputs['Image'], color.inputs['Image'])\r\n links.new(color.outputs['Image'], composite.inputs['Image'])\r\n links.new(color.outputs['Image'], viewer.inputs['Image'])\r\n \r\n return {\r\n 'render': render,\r\n 'glare': glare,\r\n 'color': color,\r\n 'composite': composite\r\n }\r\n\r\n\r\ndef create_energy_orb(\r\n radius: float = 0.3,\r\n color: tuple = (0.2, 0.6, 1),\r\n intensity: float = 30.0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"EnergyOrb\"\r\n) -> dict:\r\n \"\"\"\r\n Create a glowing energy orb with inner core.\r\n \r\n Args:\r\n radius: Orb radius\r\n color: RGB glow color\r\n intensity: Glow strength\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with orb parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Outer glow sphere\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=radius,\r\n subdivisions=3,\r\n location=location\r\n )\r\n outer = bpy.context.active_object\r\n outer.name = f\"{name}_Outer\"\r\n \r\n outer_mat = create_glow_material(f\"{name}_OuterMat\", color, intensity * 0.5)\r\n outer_mat.blend_method = 'BLEND'\r\n outer_mat.node_tree.nodes.get(\"Principled BSDF\").inputs['Alpha'].default_value = 0.3\r\n outer.data.materials.append(outer_mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n result['outer'] = outer\r\n \r\n # Inner bright core\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=radius * 0.4,\r\n subdivisions=2,\r\n location=location\r\n )\r\n inner = bpy.context.active_object\r\n inner.name = f\"{name}_Core\"\r\n \r\n inner_mat = create_glow_material(f\"{name}_CoreMat\", (1, 1, 1), intensity)\r\n inner.data.materials.append(inner_mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n result['core'] = inner\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create neon tubes\r\n create_neon_tube(color=(1, 0, 0.5), location=(0, 0, 0))\r\n create_neon_tube(color=(0, 1, 0.5), location=(0.5, 0, 0))\r\n create_neon_tube(color=(0, 0.5, 1), location=(-0.5, 0, 0))\r\n \r\n # Create energy orb\r\n create_energy_orb(location=(0, 2, 0.5))\r\n \r\n # Set up bloom\r\n setup_bloom_compositing(threshold=0.5)\r\n \r\n # Use Cycles for emission\r\n bpy.context.scene.render.engine = 'CYCLES'\r\n \r\n print(\"Created glow effects with bloom compositing\")","category":"tasks/effects","source_file":"tasks\\effects\\glow_bloom.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Lightning Effect Generator. Generates lightning bolt effects for storm scenes.","output":"import bpy\r\nimport random\r\nimport math\r\n\r\n\r\ndef create_lightning_bolt(\r\n start: tuple = (0, 0, 10),\r\n end: tuple = (0, 0, 0),\r\n branches: int = 4,\r\n segment_count: int = 10,\r\n jitter: float = 0.5,\r\n thickness: float = 0.05,\r\n glow_color: tuple = (0.5, 0.7, 1.0),\r\n seed: int = 42,\r\n name: str = \"Lightning\"\r\n) -> dict:\r\n \"\"\"\r\n Create a lightning bolt effect.\r\n \r\n Args:\r\n start: Start point (usually sky)\r\n end: End point (usually ground)\r\n branches: Number of branches\r\n segment_count: Segments per bolt\r\n jitter: Zigzag amount\r\n thickness: Bolt thickness\r\n glow_color: RGB glow color\r\n seed: Random seed\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with lightning parts\r\n \"\"\"\r\n random.seed(seed)\r\n result = {}\r\n \r\n # Main bolt\r\n main_bolt = _create_bolt_curve(\r\n start, end, segment_count, jitter, thickness, glow_color, name\r\n )\r\n result['main_bolt'] = main_bolt\r\n \r\n # Branches\r\n branch_bolts = []\r\n for i in range(branches):\r\n # Branch from random point on main bolt\r\n t = random.uniform(0.2, 0.7)\r\n branch_start = (\r\n start[0] + (end[0] - start[0]) * t + random.uniform(-jitter, jitter),\r\n start[1] + (end[1] - start[1]) * t + random.uniform(-jitter, jitter),\r\n start[2] + (end[2] - start[2]) * t\r\n )\r\n branch_end = (\r\n branch_start[0] + random.uniform(-2, 2),\r\n branch_start[1] + random.uniform(-2, 2),\r\n branch_start[2] - random.uniform(1, 3)\r\n )\r\n \r\n branch = _create_bolt_curve(\r\n branch_start, branch_end,\r\n segment_count // 2, jitter * 0.7, thickness * 0.5, glow_color,\r\n f\"{name}_Branch_{i}\"\r\n )\r\n branch_bolts.append(branch)\r\n \r\n result['branches'] = branch_bolts\r\n \r\n # Impact light\r\n bpy.ops.object.light_add(type='POINT', location=end)\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 5000\r\n light.data.color = glow_color\r\n light.data.shadow_soft_size = 2\r\n result['light'] = light\r\n \r\n return result\r\n\r\n\r\ndef _create_bolt_curve(\r\n start: tuple,\r\n end: tuple,\r\n segments: int,\r\n jitter: float,\r\n thickness: float,\r\n color: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create zigzag bolt curve.\"\"\"\r\n curve_data = bpy.data.curves.new(f\"{name}_Curve\", 'CURVE')\r\n curve_data.dimensions = '3D'\r\n curve_data.bevel_depth = thickness\r\n curve_data.bevel_resolution = 2\r\n \r\n spline = curve_data.splines.new('POLY')\r\n spline.points.add(segments - 1)\r\n \r\n for i in range(segments):\r\n t = i / (segments - 1)\r\n \r\n # Interpolate with jitter\r\n x = start[0] + (end[0] - start[0]) * t\r\n y = start[1] + (end[1] - start[1]) * t\r\n z = start[2] + (end[2] - start[2]) * t\r\n \r\n if i > 0 and i < segments - 1:\r\n x += random.uniform(-jitter, jitter)\r\n y += random.uniform(-jitter, jitter)\r\n \r\n spline.points[i].co = (x, y, z, 1)\r\n \r\n bolt = bpy.data.objects.new(name, curve_data)\r\n bpy.context.collection.objects.link(bolt)\r\n \r\n # Emission material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (1, 1, 1, 1)\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 50.0\r\n bolt.data.materials.append(mat)\r\n \r\n return bolt\r\n\r\n\r\ndef animate_lightning_flash(\r\n light: bpy.types.Object,\r\n start_frame: int = 1,\r\n duration: int = 5\r\n) -> None:\r\n \"\"\"Animate lightning flash on/off.\"\"\"\r\n light.data.energy = 0\r\n light.data.keyframe_insert('energy', frame=start_frame)\r\n \r\n # Flash on\r\n light.data.energy = 5000\r\n light.data.keyframe_insert('energy', frame=start_frame + 1)\r\n \r\n # Flash off\r\n light.data.energy = 0\r\n light.data.keyframe_insert('energy', frame=start_frame + 2)\r\n \r\n # Secondary flash\r\n light.data.energy = 3000\r\n light.data.keyframe_insert('energy', frame=start_frame + 3)\r\n \r\n light.data.energy = 0\r\n light.data.keyframe_insert('energy', frame=start_frame + duration)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n lightning = create_lightning_bolt(\r\n start=(0, 0, 10),\r\n end=(1, 0.5, 0),\r\n branches=3\r\n )\r\n \r\n bpy.context.scene.render.engine = 'CYCLES'\r\n print(\"Created lightning bolt\")","category":"tasks/effects","source_file":"tasks\\effects\\lightning_effect.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Magic Spell Effect. Creates magic spell effects with particles and glowing elements.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_magic_projectile(\r\n location: tuple = (0, 0, 1),\r\n color: tuple = (0.2, 0.5, 1.0),\r\n size: float = 0.2,\r\n trail_length: float = 1.0,\r\n animated: bool = True,\r\n name: str = \"MagicProjectile\"\r\n) -> dict:\r\n \"\"\"\r\n Create a magic projectile with trail.\r\n \r\n Args:\r\n location: Starting position\r\n color: RGB glow color\r\n size: Projectile size\r\n trail_length: Trail particle length\r\n animated: Add motion animation\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with projectile parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Core sphere\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=size,\r\n subdivisions=2,\r\n location=location\r\n )\r\n core = bpy.context.active_object\r\n core.name = f\"{name}_Core\"\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Glow material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 30.0\r\n core.data.materials.append(mat)\r\n \r\n result['core'] = core\r\n \r\n # Outer glow\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=size * 2,\r\n subdivisions=2,\r\n location=location\r\n )\r\n glow = bpy.context.active_object\r\n glow.name = f\"{name}_Glow\"\r\n \r\n glow_mat = bpy.data.materials.new(f\"{name}_GlowMat\")\r\n glow_mat.blend_method = 'BLEND'\r\n bsdf = glow_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 5.0\r\n bsdf.inputs['Alpha'].default_value = 0.3\r\n glow.data.materials.append(glow_mat)\r\n \r\n result['glow'] = glow\r\n \r\n # Trail particles\r\n bpy.ops.mesh.primitive_ico_sphere_add(radius=0.01, location=location)\r\n emitter = bpy.context.active_object\r\n emitter.name = f\"{name}_Emitter\"\r\n \r\n bpy.ops.object.particle_system_add()\r\n ps = emitter.particle_systems[-1]\r\n settings = ps.settings\r\n \r\n settings.count = 100\r\n settings.lifetime = 20\r\n settings.emit_from = 'VERT'\r\n settings.normal_factor = 0\r\n settings.factor_random = 0.5\r\n settings.render_type = 'HALO'\r\n settings.particle_size = size * 0.3\r\n \r\n result['emitter'] = emitter\r\n \r\n # Parent glow to core\r\n glow.parent = core\r\n emitter.parent = core\r\n \r\n # Animation\r\n if animated:\r\n core.location = location\r\n core.keyframe_insert(data_path=\"location\", frame=1)\r\n core.location = (location[0], location[1] - 10, location[2])\r\n core.keyframe_insert(data_path=\"location\", frame=60)\r\n \r\n return result\r\n\r\n\r\ndef create_magic_circle(\r\n radius: float = 1.5,\r\n rune_count: int = 6,\r\n color: tuple = (1.0, 0.5, 0.2),\r\n animated: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"MagicCircle\"\r\n) -> dict:\r\n \"\"\"\r\n Create a magic summoning circle.\r\n \r\n Args:\r\n radius: Circle radius\r\n rune_count: Number of rune symbols\r\n color: RGB glow color\r\n animated: Add rotation animation\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with circle parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Outer ring\r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=radius,\r\n minor_radius=0.02,\r\n location=location\r\n )\r\n outer = bpy.context.active_object\r\n outer.name = f\"{name}_Outer\"\r\n outer.rotation_euler.x = math.radians(90)\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 20.0\r\n outer.data.materials.append(mat)\r\n \r\n result['outer_ring'] = outer\r\n \r\n # Inner ring\r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=radius * 0.7,\r\n minor_radius=0.015,\r\n location=location\r\n )\r\n inner = bpy.context.active_object\r\n inner.name = f\"{name}_Inner\"\r\n inner.rotation_euler.x = math.radians(90)\r\n inner.data.materials.append(mat)\r\n \r\n result['inner_ring'] = inner\r\n \r\n # Rune symbols (simple spheres for now)\r\n runes = []\r\n for i in range(rune_count):\r\n angle = (i / rune_count) * 2 * math.pi\r\n pos_x = location[0] + math.cos(angle) * radius * 0.85\r\n pos_y = location[1] + math.sin(angle) * radius * 0.85\r\n \r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=0.05,\r\n location=(pos_x, pos_y, location[2])\r\n )\r\n rune = bpy.context.active_object\r\n rune.name = f\"{name}_Rune_{i}\"\r\n rune.data.materials.append(mat)\r\n runes.append(rune)\r\n \r\n result['runes'] = runes\r\n \r\n # Light\r\n bpy.ops.object.light_add(\r\n type='POINT',\r\n location=(location[0], location[1], location[2] + 0.5)\r\n )\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 100\r\n light.data.color = color\r\n result['light'] = light\r\n \r\n # Animation\r\n if animated:\r\n for ring in [outer, inner]:\r\n ring.rotation_euler = (math.radians(90), 0, 0)\r\n ring.keyframe_insert(data_path=\"rotation_euler\", frame=1)\r\n ring.rotation_euler = (math.radians(90), 0, math.radians(360))\r\n ring.keyframe_insert(data_path=\"rotation_euler\", frame=120)\r\n \r\n if ring.animation_data and ring.animation_data.action:\r\n for fc in ring.animation_data.action.fcurves:\r\n fc.modifiers.new('CYCLES')\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_magic_projectile(location=(0, 5, 1))\r\n create_magic_circle(location=(0, 0, 0))\r\n \r\n bpy.context.scene.render.engine = 'CYCLES'\r\n print(\"Created magic spell effects\")","category":"tasks/effects","source_file":"tasks\\effects\\magic_spell.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Muzzle Flash Effect. Creates a muzzle flash effect for weapons with light and geometry.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_muzzle_flash(\r\n location: tuple = (0, 0, 0),\r\n direction: tuple = (0, -1, 0),\r\n size: float = 0.3,\r\n color: tuple = (1.0, 0.7, 0.2),\r\n intensity: float = 500,\r\n frame: int = 1,\r\n duration: int = 3,\r\n name: str = \"MuzzleFlash\"\r\n) -> dict:\r\n \"\"\"\r\n Create a muzzle flash effect.\r\n \r\n Args:\r\n location: Flash position\r\n direction: Direction of fire (normalized)\r\n size: Flash size\r\n color: RGB flash color\r\n intensity: Light intensity\r\n frame: Start frame\r\n duration: Flash duration in frames\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with flash components\r\n \"\"\"\r\n result = {}\r\n \r\n # === FLASH MESH ===\r\n # Create cone for flash shape\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=size * 0.3,\r\n radius2=0,\r\n depth=size,\r\n location=location\r\n )\r\n flash = bpy.context.active_object\r\n flash.name = f\"{name}_Cone\"\r\n \r\n # Orient to direction\r\n from mathutils import Vector\r\n dir_vec = Vector(direction).normalized()\r\n flash.rotation_euler = dir_vec.to_track_quat('-Z', 'Y').to_euler()\r\n \r\n # Move tip to location\r\n flash.location = (\r\n location[0] + dir_vec.x * size * 0.5,\r\n location[1] + dir_vec.y * size * 0.5,\r\n location[2] + dir_vec.z * size * 0.5\r\n )\r\n \r\n # Emission material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n nodes = mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = intensity * 0.1\r\n flash.data.materials.append(mat)\r\n \r\n result['flash'] = flash\r\n \r\n # === POINT LIGHT ===\r\n bpy.ops.object.light_add(type='POINT', location=location)\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = intensity\r\n light.data.color = color\r\n light.data.shadow_soft_size = size * 0.5\r\n \r\n result['light'] = light\r\n \r\n # === SPARKS (small spheres) ===\r\n sparks = []\r\n for i in range(5):\r\n angle = random.uniform(0, 2 * math.pi)\r\n spread = random.uniform(0, size * 0.3)\r\n \r\n spark_pos = (\r\n location[0] + math.cos(angle) * spread,\r\n location[1] + math.sin(angle) * spread,\r\n location[2] + random.uniform(-size * 0.1, size * 0.1)\r\n )\r\n \r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=size * 0.02,\r\n location=spark_pos\r\n )\r\n spark = bpy.context.active_object\r\n spark.name = f\"{name}_Spark_{i}\"\r\n spark.data.materials.append(mat)\r\n sparks.append(spark)\r\n \r\n result['sparks'] = sparks\r\n \r\n # === ANIMATION ===\r\n # Keyframe visibility\r\n all_objects = [flash, light] + sparks\r\n \r\n for obj in all_objects:\r\n # Hidden before frame\r\n obj.hide_viewport = True\r\n obj.hide_render = True\r\n obj.keyframe_insert(data_path=\"hide_viewport\", frame=frame - 1)\r\n obj.keyframe_insert(data_path=\"hide_render\", frame=frame - 1)\r\n \r\n # Visible at frame\r\n obj.hide_viewport = False\r\n obj.hide_render = False\r\n obj.keyframe_insert(data_path=\"hide_viewport\", frame=frame)\r\n obj.keyframe_insert(data_path=\"hide_render\", frame=frame)\r\n \r\n # Hidden after duration\r\n obj.hide_viewport = True\r\n obj.hide_render = True\r\n obj.keyframe_insert(data_path=\"hide_viewport\", frame=frame + duration)\r\n obj.keyframe_insert(data_path=\"hide_render\", frame=frame + duration)\r\n \r\n # Light intensity animation\r\n light.data.energy = intensity\r\n light.data.keyframe_insert(data_path=\"energy\", frame=frame)\r\n light.data.energy = 0\r\n light.data.keyframe_insert(data_path=\"energy\", frame=frame + duration)\r\n \r\n return result\r\n\r\n\r\ndef create_muzzle_flash_sequence(\r\n location: tuple,\r\n direction: tuple,\r\n count: int = 3,\r\n interval: int = 5,\r\n start_frame: int = 1\r\n) -> list:\r\n \"\"\"Create multiple muzzle flashes in sequence.\"\"\"\r\n flashes = []\r\n \r\n for i in range(count):\r\n frame = start_frame + i * interval\r\n flash = create_muzzle_flash(\r\n location=location,\r\n direction=direction,\r\n frame=frame,\r\n name=f\"MuzzleFlash_{i+1}\"\r\n )\r\n flashes.append(flash)\r\n \r\n return flashes\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Single flash\r\n create_muzzle_flash(location=(0, 0, 1), direction=(0, -1, 0))\r\n \r\n # Sequence\r\n create_muzzle_flash_sequence((2, 0, 1), (0, -1, 0), count=5, interval=8)\r\n \r\n print(\"Created muzzle flash effects\")","category":"tasks/effects","source_file":"tasks\\effects\\muzzle_flash.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Portal Effect. Creates an animated magic portal effect with rotating rings.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_portal(\r\n radius: float = 1.0,\r\n color: tuple = (0.2, 0.5, 1.0),\r\n inner_color: tuple = (0.8, 0.9, 1.0),\r\n ring_count: int = 3,\r\n rotation_speed: float = 1.0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Portal\"\r\n) -> dict:\r\n \"\"\"\r\n Create an animated portal effect.\r\n \r\n Args:\r\n radius: Portal radius\r\n color: RGB outer glow color\r\n inner_color: RGB inner glow color\r\n ring_count: Number of rotating rings\r\n rotation_speed: Ring rotation speed\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with portal components\r\n \"\"\"\r\n result = {}\r\n result['rings'] = []\r\n \r\n # === INNER GLOW DISK ===\r\n bpy.ops.mesh.primitive_circle_add(\r\n radius=radius * 0.9,\r\n fill_type='NGON',\r\n location=location\r\n )\r\n inner = bpy.context.active_object\r\n inner.name = f\"{name}_Inner\"\r\n \r\n inner_mat = bpy.data.materials.new(f\"{name}_InnerMat\")\r\n inner_mat.blend_method = 'BLEND'\r\n nodes = inner_mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*inner_color, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*inner_color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 5.0\r\n bsdf.inputs['Alpha'].default_value = 0.7\r\n inner.data.materials.append(inner_mat)\r\n \r\n result['inner'] = inner\r\n \r\n # === ROTATING RINGS ===\r\n for i in range(ring_count):\r\n ring_radius = radius * (0.8 + i * 0.15)\r\n \r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=ring_radius,\r\n minor_radius=0.02,\r\n location=location\r\n )\r\n ring = bpy.context.active_object\r\n ring.name = f\"{name}_Ring_{i+1}\"\r\n \r\n # Ring material\r\n ring_mat = bpy.data.materials.new(f\"{name}_RingMat_{i}\")\r\n nodes = ring_mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n # Gradient color based on ring index\r\n t = i / max(ring_count - 1, 1)\r\n ring_color = (\r\n color[0] + (inner_color[0] - color[0]) * t,\r\n color[1] + (inner_color[1] - color[1]) * t,\r\n color[2] + (inner_color[2] - color[2]) * t\r\n )\r\n \r\n bsdf.inputs['Base Color'].default_value = (*ring_color, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*ring_color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 10.0 - i * 2\r\n ring.data.materials.append(ring_mat)\r\n \r\n # Animation - different rotation speeds/axes\r\n _animate_ring_rotation(ring, rotation_speed * (1 + i * 0.3), axis=i % 3)\r\n \r\n result['rings'].append(ring)\r\n \r\n # === OUTER GLOW RING ===\r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=radius,\r\n minor_radius=0.05,\r\n location=location\r\n )\r\n outer = bpy.context.active_object\r\n outer.name = f\"{name}_Outer\"\r\n \r\n outer_mat = bpy.data.materials.new(f\"{name}_OuterMat\")\r\n nodes = outer_mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 15.0\r\n outer.data.materials.append(outer_mat)\r\n \r\n result['outer'] = outer\r\n \r\n # === LIGHT ===\r\n bpy.ops.object.light_add(\r\n type='POINT',\r\n location=(location[0], location[1] - 0.5, location[2])\r\n )\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 100\r\n light.data.color = color\r\n \r\n result['light'] = light\r\n \r\n return result\r\n\r\n\r\ndef _animate_ring_rotation(\r\n ring: bpy.types.Object,\r\n speed: float,\r\n axis: int = 2\r\n) -> None:\r\n \"\"\"Animate ring rotation.\"\"\"\r\n ring.rotation_euler = (0, 0, 0)\r\n ring.keyframe_insert(data_path=\"rotation_euler\", frame=1)\r\n \r\n # Rotate different axes for variety\r\n if axis == 0:\r\n ring.rotation_euler.x = math.radians(360 * speed)\r\n elif axis == 1:\r\n ring.rotation_euler.y = math.radians(360 * speed)\r\n else:\r\n ring.rotation_euler.z = math.radians(360 * speed)\r\n \r\n ring.keyframe_insert(data_path=\"rotation_euler\", frame=250)\r\n \r\n # Make cyclic\r\n if ring.animation_data and ring.animation_data.action:\r\n for fcurve in ring.animation_data.action.fcurves:\r\n fcurve.modifiers.new('CYCLES')\r\n for kp in fcurve.keyframe_points:\r\n kp.interpolation = 'LINEAR'\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Blue portal\r\n create_portal(color=(0.2, 0.5, 1.0), location=(0, 0, 1))\r\n \r\n # Orange portal\r\n create_portal(color=(1.0, 0.5, 0.1), location=(3, 0, 1))\r\n \r\n bpy.context.scene.render.engine = 'CYCLES'\r\n print(\"Created portal effects\")","category":"tasks/effects","source_file":"tasks\\effects\\portal_effect.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Rain Effect Generator. Creates rain particle effect with optional splashes.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_rain_effect(\r\n area_size: tuple = (10, 10),\r\n height: float = 10,\r\n density: int = 2000,\r\n speed: float = 1.0,\r\n angle: float = 0,\r\n with_splashes: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Rain\"\r\n) -> dict:\r\n \"\"\"\r\n Create a rain particle effect.\r\n \r\n Args:\r\n area_size: XY area covered by rain\r\n height: Rain fall height\r\n density: Number of rain particles\r\n speed: Fall speed multiplier\r\n angle: Wind angle in degrees\r\n with_splashes: Add splash particles on ground\r\n location: Center position\r\n name: Effect name\r\n \r\n Returns:\r\n Dictionary with rain components\r\n \"\"\"\r\n result = {}\r\n \r\n # === RAIN EMITTER ===\r\n bpy.ops.mesh.primitive_plane_add(\r\n size=1,\r\n location=(location[0], location[1], location[2] + height)\r\n )\r\n emitter = bpy.context.active_object\r\n emitter.name = f\"{name}_Emitter\"\r\n emitter.scale = (area_size[0]/2, area_size[1]/2, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n emitter.hide_render = True\r\n \r\n # Add particle system\r\n bpy.ops.object.particle_system_add()\r\n ps = emitter.particle_systems[-1]\r\n ps.name = f\"{name}_Particles\"\r\n settings = ps.settings\r\n settings.name = f\"{name}_Settings\"\r\n \r\n # Rain settings\r\n settings.count = density\r\n settings.lifetime = int(height / (10 * speed) * 24) # Frames to fall\r\n settings.emit_from = 'FACE'\r\n settings.use_emit_random = True\r\n \r\n # Physics\r\n settings.normal_factor = 0\r\n settings.object_align_factor = (\r\n math.sin(math.radians(angle)) * 0.2, # Wind\r\n 0,\r\n -10 * speed # Downward\r\n )\r\n settings.factor_random = 0.1\r\n \r\n # Render as lines (streaks)\r\n settings.render_type = 'LINE'\r\n settings.line_length_tail = 2.0\r\n settings.line_length_head = 0.0\r\n \r\n # Material for rain\r\n rain_mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = rain_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.85, 0.9, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.1\r\n bsdf.inputs['Transmission Weight'].default_value = 0.5\r\n \r\n settings.material = 1\r\n if not emitter.data.materials:\r\n emitter.data.materials.append(rain_mat)\r\n \r\n result['emitter'] = emitter\r\n result['particle_system'] = ps\r\n \r\n # === SPLASH PARTICLES ===\r\n if with_splashes:\r\n splash = _create_rain_splashes(area_size, density // 4, location, name)\r\n result['splashes'] = splash\r\n \r\n return result\r\n\r\n\r\ndef _create_rain_splashes(\r\n area_size: tuple,\r\n count: int,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create rain splash particles on ground.\"\"\"\r\n # Ground plane emitter\r\n bpy.ops.mesh.primitive_plane_add(\r\n size=1,\r\n location=(location[0], location[1], location[2] + 0.01)\r\n )\r\n splash_emitter = bpy.context.active_object\r\n splash_emitter.name = f\"{name}_SplashEmitter\"\r\n splash_emitter.scale = (area_size[0]/2, area_size[1]/2, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n splash_emitter.hide_render = True\r\n \r\n bpy.ops.object.particle_system_add()\r\n ps = splash_emitter.particle_systems[-1]\r\n settings = ps.settings\r\n \r\n settings.count = count\r\n settings.lifetime = 5\r\n settings.emit_from = 'FACE'\r\n settings.use_emit_random = True\r\n \r\n # Splash upward\r\n settings.normal_factor = 2.0\r\n settings.factor_random = 0.5\r\n \r\n settings.render_type = 'HALO'\r\n settings.particle_size = 0.01\r\n \r\n return splash_emitter\r\n\r\n\r\ndef create_snow_effect(\r\n area_size: tuple = (10, 10),\r\n height: float = 8,\r\n density: int = 1000,\r\n flake_size: float = 0.02,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Snow\"\r\n) -> dict:\r\n \"\"\"\r\n Create a snow particle effect.\r\n \r\n Args:\r\n area_size: XY area covered\r\n height: Fall height\r\n density: Number of snowflakes\r\n flake_size: Size of flakes\r\n location: Center position\r\n name: Effect name\r\n \r\n Returns:\r\n Dictionary with snow components\r\n \"\"\"\r\n result = {}\r\n \r\n bpy.ops.mesh.primitive_plane_add(\r\n size=1,\r\n location=(location[0], location[1], location[2] + height)\r\n )\r\n emitter = bpy.context.active_object\r\n emitter.name = f\"{name}_Emitter\"\r\n emitter.scale = (area_size[0]/2, area_size[1]/2, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n emitter.hide_render = True\r\n \r\n bpy.ops.object.particle_system_add()\r\n ps = emitter.particle_systems[-1]\r\n settings = ps.settings\r\n \r\n # Snow falls slower, drifts more\r\n settings.count = density\r\n settings.lifetime = int(height / 2 * 24) # Slower fall\r\n settings.emit_from = 'FACE'\r\n \r\n settings.normal_factor = 0\r\n settings.object_align_factor = (0, 0, -2) # Slow fall\r\n settings.brownian_factor = 0.3 # Drift\r\n settings.factor_random = 0.5\r\n \r\n # Render as spheres\r\n settings.render_type = 'HALO'\r\n settings.particle_size = flake_size\r\n settings.size_random = 0.5\r\n \r\n result['emitter'] = emitter\r\n result['particle_system'] = ps\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_rain_effect(area_size=(8, 8), density=1500)\r\n create_snow_effect(area_size=(8, 8), location=(12, 0, 0), density=800)\r\n \r\n print(\"Created rain and snow effects\")","category":"tasks/effects","source_file":"tasks\\effects\\rain_effect.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Smoke and Fire Effects. Creates smoke and fire simulation effects.","output":"import bpy\r\n\r\n\r\ndef create_smoke_effect(\r\n location: tuple = (0, 0, 0),\r\n domain_size: tuple = (4, 4, 6),\r\n resolution: int = 64,\r\n smoke_color: tuple = (0.5, 0.5, 0.5),\r\n name_prefix: str = \"Smoke\"\r\n) -> dict:\r\n \"\"\"Create a smoke simulation.\"\"\"\r\n result = {}\r\n \r\n # Domain\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=location)\r\n domain = bpy.context.active_object\r\n domain.name = f\"{name_prefix}_Domain\"\r\n domain.scale = [s/2 for s in domain_size]\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n bpy.ops.object.modifier_add(type='FLUID')\r\n domain.modifiers[\"Fluid\"].fluid_type = 'DOMAIN'\r\n settings = domain.modifiers[\"Fluid\"].domain_settings\r\n settings.domain_type = 'GAS'\r\n settings.resolution_max = resolution\r\n \r\n result['domain'] = domain\r\n \r\n # Flow\r\n flow_loc = (location[0], location[1], location[2] - domain_size[2]/2 + 0.5)\r\n bpy.ops.mesh.primitive_ico_sphere_add(radius=0.3, location=flow_loc)\r\n flow = bpy.context.active_object\r\n flow.name = f\"{name_prefix}_Flow\"\r\n \r\n bpy.ops.object.modifier_add(type='FLUID')\r\n flow.modifiers[\"Fluid\"].fluid_type = 'FLOW'\r\n flow.modifiers[\"Fluid\"].flow_settings.flow_type = 'SMOKE'\r\n flow.modifiers[\"Fluid\"].flow_settings.smoke_color = smoke_color\r\n \r\n result['flow'] = flow\r\n return result\r\n\r\n\r\ndef create_fire_effect(\r\n location: tuple = (0, 0, 0),\r\n domain_size: tuple = (3, 3, 5),\r\n resolution: int = 64,\r\n name_prefix: str = \"Fire\"\r\n) -> dict:\r\n \"\"\"Create a fire simulation.\"\"\"\r\n result = {}\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=location)\r\n domain = bpy.context.active_object\r\n domain.name = f\"{name_prefix}_Domain\"\r\n domain.scale = [s/2 for s in domain_size]\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n bpy.ops.object.modifier_add(type='FLUID')\r\n domain.modifiers[\"Fluid\"].fluid_type = 'DOMAIN'\r\n settings = domain.modifiers[\"Fluid\"].domain_settings\r\n settings.domain_type = 'GAS'\r\n settings.resolution_max = resolution\r\n settings.beta = 2.0\r\n \r\n result['domain'] = domain\r\n \r\n flow_loc = (location[0], location[1], location[2] - domain_size[2]/2 + 0.3)\r\n bpy.ops.mesh.primitive_plane_add(size=1, location=flow_loc)\r\n flow = bpy.context.active_object\r\n flow.name = f\"{name_prefix}_Flow\"\r\n \r\n bpy.ops.object.modifier_add(type='FLUID')\r\n flow.modifiers[\"Fluid\"].fluid_type = 'FLOW'\r\n flow.modifiers[\"Fluid\"].flow_settings.flow_type = 'BOTH'\r\n flow.modifiers[\"Fluid\"].flow_settings.fuel_amount = 1.0\r\n \r\n result['flow'] = flow\r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n smoke = create_smoke_effect()\r\n fire = create_fire_effect(location=(5, 0, 0))\r\n print(\"Created smoke and fire effects\")","category":"tasks/effects","source_file":"tasks\\effects\\smoke_fire.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Spark Effect Generator. Generates spark particle effects for impacts and welding.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_spark_emitter(\r\n location: tuple = (0, 0, 0),\r\n direction: tuple = (0, 0, 1),\r\n spark_count: int = 200,\r\n lifetime: int = 30,\r\n velocity: float = 5.0,\r\n spread: float = 45,\r\n color: tuple = (1.0, 0.6, 0.1),\r\n name: str = \"Sparks\"\r\n) -> dict:\r\n \"\"\"\r\n Create spark particle emitter.\r\n \r\n Args:\r\n location: Emission point\r\n direction: Main direction of sparks\r\n spark_count: Number of particles\r\n lifetime: Particle lifetime in frames\r\n velocity: Initial velocity\r\n spread: Cone spread angle\r\n color: RGB spark color\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with emitter parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Emitter object\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=0.02,\r\n location=location\r\n )\r\n emitter = bpy.context.active_object\r\n emitter.name = name\r\n emitter.hide_render = True\r\n result['emitter'] = emitter\r\n \r\n # Particle system\r\n bpy.ops.object.particle_system_add()\r\n psys = emitter.particle_systems[0]\r\n psys.name = f\"{name}_Particles\"\r\n \r\n settings = psys.settings\r\n settings.name = f\"{name}_Settings\"\r\n settings.type = 'EMITTER'\r\n settings.count = spark_count\r\n settings.lifetime = lifetime\r\n settings.frame_start = 1\r\n settings.frame_end = 10\r\n \r\n # Velocity\r\n settings.normal_factor = velocity\r\n settings.factor_random = velocity * 0.5\r\n settings.tangent_factor = velocity * 0.3\r\n \r\n # Physics\r\n settings.effector_weights.gravity = 1.0\r\n settings.mass = 0.001\r\n settings.particle_size = 0.005\r\n settings.size_random = 0.5\r\n \r\n # Render as halo\r\n settings.render_type = 'HALO'\r\n \r\n result['particle_system'] = psys\r\n \r\n # Spark material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 20.0\r\n emitter.data.materials.append(mat)\r\n \r\n result['material'] = mat\r\n \r\n # Point light for glow\r\n bpy.ops.object.light_add(type='POINT', location=location)\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 50\r\n light.data.color = color\r\n light.data.shadow_soft_size = 0.1\r\n result['light'] = light\r\n \r\n return result\r\n\r\n\r\ndef create_impact_sparks(\r\n location: tuple = (0, 0, 0),\r\n surface_normal: tuple = (0, 0, 1),\r\n intensity: float = 1.0,\r\n name: str = \"ImpactSparks\"\r\n) -> dict:\r\n \"\"\"\r\n Create sparks from an impact point.\r\n \r\n Args:\r\n location: Impact location\r\n surface_normal: Surface normal direction\r\n intensity: Effect intensity multiplier\r\n name: Object name\r\n \"\"\"\r\n return create_spark_emitter(\r\n location=location,\r\n direction=surface_normal,\r\n spark_count=int(150 * intensity),\r\n lifetime=20,\r\n velocity=8.0 * intensity,\r\n spread=60,\r\n color=(1.0, 0.7, 0.2),\r\n name=name\r\n )\r\n\r\n\r\ndef create_welding_sparks(\r\n location: tuple = (0, 0, 0),\r\n name: str = \"WeldingSparks\"\r\n) -> dict:\r\n \"\"\"Create continuous welding spark stream.\"\"\"\r\n result = create_spark_emitter(\r\n location=location,\r\n spark_count=500,\r\n lifetime=15,\r\n velocity=3.0,\r\n spread=30,\r\n color=(1.0, 0.9, 0.5),\r\n name=name\r\n )\r\n \r\n # Make continuous\r\n settings = result['particle_system'].settings\r\n settings.frame_end = 250\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_spark_emitter(location=(0, 0, 0))\r\n create_impact_sparks(location=(1, 0, 0))\r\n \r\n print(\"Created spark effects\")","category":"tasks/effects","source_file":"tasks\\effects\\spark_effect.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Campfire Generator. Generates campfires with logs, fire, and ambient lighting.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_campfire(\r\n size: float = 0.5,\r\n lit: bool = True,\r\n with_stones: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Campfire\"\r\n) -> dict:\r\n \"\"\"\r\n Create a campfire.\r\n \r\n Args:\r\n size: Campfire size\r\n lit: Add fire effect\r\n with_stones: Add surrounding stones\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with campfire parts\r\n \"\"\"\r\n result = {}\r\n \r\n # === LOGS ===\r\n logs = _create_logs(size, location, name)\r\n result['logs'] = logs\r\n \r\n # === STONES ===\r\n if with_stones:\r\n stones = _create_fire_stones(size, location, name)\r\n result['stones'] = stones\r\n \r\n # === FIRE ===\r\n if lit:\r\n fire = _create_fire_effect(size, location, name)\r\n result.update(fire)\r\n \r\n return result\r\n\r\n\r\ndef _create_logs(\r\n size: float,\r\n location: tuple,\r\n name: str\r\n) -> list:\r\n \"\"\"Create log arrangement.\"\"\"\r\n logs = []\r\n log_mat = bpy.data.materials.new(f\"{name}_LogMat\")\r\n bsdf = log_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.2, 0.12, 0.06, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n \r\n # Create 3-4 logs\r\n for i in range(4):\r\n angle = (i / 4) * 2 * math.pi + random.uniform(-0.3, 0.3)\r\n log_length = size * random.uniform(0.6, 0.9)\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=size * 0.08,\r\n depth=log_length,\r\n location=(\r\n location[0] + math.cos(angle) * size * 0.2,\r\n location[1] + math.sin(angle) * size * 0.2,\r\n location[2] + size * 0.1\r\n )\r\n )\r\n log = bpy.context.active_object\r\n log.name = f\"{name}_Log_{i}\"\r\n log.rotation_euler = (\r\n random.uniform(-0.3, 0.3),\r\n math.radians(90) + random.uniform(-0.2, 0.2),\r\n angle + math.radians(90)\r\n )\r\n log.data.materials.append(log_mat)\r\n logs.append(log)\r\n \r\n return logs\r\n\r\n\r\ndef _create_fire_stones(\r\n size: float,\r\n location: tuple,\r\n name: str\r\n) -> list:\r\n \"\"\"Create surrounding stones.\"\"\"\r\n stones = []\r\n stone_mat = bpy.data.materials.new(f\"{name}_StoneMat\")\r\n bsdf = stone_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.32, 0.3, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n \r\n stone_count = 8\r\n for i in range(stone_count):\r\n angle = (i / stone_count) * 2 * math.pi\r\n stone_size = size * random.uniform(0.08, 0.15)\r\n \r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=stone_size,\r\n subdivisions=1,\r\n location=(\r\n location[0] + math.cos(angle) * size * 0.6,\r\n location[1] + math.sin(angle) * size * 0.6,\r\n location[2] + stone_size * 0.5\r\n )\r\n )\r\n stone = bpy.context.active_object\r\n stone.name = f\"{name}_Stone_{i}\"\r\n stone.scale.z = random.uniform(0.5, 0.8)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Deform\r\n for v in stone.data.vertices:\r\n v.co += (random.uniform(-0.02, 0.02),\r\n random.uniform(-0.02, 0.02),\r\n random.uniform(-0.02, 0.02))\r\n \r\n stone.data.materials.append(stone_mat)\r\n stones.append(stone)\r\n \r\n return stones\r\n\r\n\r\ndef _create_fire_effect(\r\n size: float,\r\n location: tuple,\r\n name: str\r\n) -> dict:\r\n \"\"\"Create fire and light.\"\"\"\r\n result = {}\r\n \r\n # Fire cone\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=size * 0.25,\r\n radius2=0,\r\n depth=size * 0.8,\r\n location=(location[0], location[1], location[2] + size * 0.5)\r\n )\r\n fire = bpy.context.active_object\r\n fire.name = f\"{name}_Fire\"\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Deform for natural shape\r\n for v in fire.data.vertices:\r\n v.co.x += random.uniform(-0.02, 0.02)\r\n v.co.y += random.uniform(-0.02, 0.02)\r\n \r\n fire_mat = bpy.data.materials.new(f\"{name}_FireMat\")\r\n fire_mat.blend_method = 'BLEND'\r\n bsdf = fire_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (1.0, 0.4, 0.0, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (1.0, 0.3, 0.0, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 25.0\r\n bsdf.inputs['Alpha'].default_value = 0.8\r\n fire.data.materials.append(fire_mat)\r\n result['fire'] = fire\r\n \r\n # Point light\r\n bpy.ops.object.light_add(\r\n type='POINT',\r\n location=(location[0], location[1], location[2] + size * 0.4)\r\n )\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 200\r\n light.data.color = (1.0, 0.6, 0.2)\r\n light.data.shadow_soft_size = size\r\n result['light'] = light\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_campfire(lit=True, location=(0, 0, 0))\r\n \r\n print(\"Created campfire\")","category":"tasks/environment","source_file":"tasks\\environment\\campfire_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Cloud Generator. Generates volumetric clouds for sky environments.","output":"import bpy\r\nimport random\r\nimport math\r\n\r\n\r\ndef create_cloud(\r\n size: tuple = (5, 3, 2),\r\n density: float = 0.5,\r\n detail: float = 5.0,\r\n location: tuple = (0, 0, 10),\r\n name: str = \"Cloud\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a volumetric cloud.\r\n \r\n Args:\r\n size: XYZ cloud dimensions\r\n density: Cloud density\r\n detail: Noise detail level\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The cloud object\r\n \"\"\"\r\n # Create base shape\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=1,\r\n subdivisions=2,\r\n location=location\r\n )\r\n cloud = bpy.context.active_object\r\n cloud.name = name\r\n cloud.scale = (size[0]/2, size[1]/2, size[2]/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Deform for cloud shape\r\n bpy.ops.object.modifier_add(type='DISPLACE')\r\n tex = bpy.data.textures.new(f\"{name}_CloudTex\", 'CLOUDS')\r\n tex.noise_scale = 1.0\r\n cloud.modifiers[\"Displace\"].texture = tex\r\n cloud.modifiers[\"Displace\"].strength = size[0] * 0.3\r\n \r\n # Volumetric material\r\n mat = _create_cloud_material(name, density, detail)\r\n cloud.data.materials.append(mat)\r\n \r\n return cloud\r\n\r\n\r\ndef _create_cloud_material(\r\n name: str,\r\n density: float,\r\n detail: float\r\n) -> bpy.types.Material:\r\n \"\"\"Create volumetric cloud material.\"\"\"\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Principled Volume\r\n volume = nodes.new('ShaderNodeVolumePrincipled')\r\n volume.location = (0, 0)\r\n volume.inputs['Color'].default_value = (1, 1, 1, 1)\r\n volume.inputs['Density'].default_value = density\r\n volume.inputs['Anisotropy'].default_value = 0.3\r\n \r\n # Noise for cloud shape\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, 0)\r\n noise.inputs['Scale'].default_value = detail\r\n noise.inputs['Detail'].default_value = 8.0\r\n \r\n # Math to control density\r\n math_node = nodes.new('ShaderNodeMath')\r\n math_node.location = (-200, 0)\r\n math_node.operation = 'MULTIPLY'\r\n math_node.inputs[1].default_value = density * 5\r\n \r\n links.new(noise.outputs['Fac'], math_node.inputs[0])\r\n links.new(math_node.outputs['Value'], volume.inputs['Density'])\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (200, 0)\r\n links.new(volume.outputs['Volume'], output.inputs['Volume'])\r\n \r\n return mat\r\n\r\n\r\ndef create_cloud_layer(\r\n cloud_count: int = 5,\r\n area_size: tuple = (50, 50),\r\n altitude: float = 15,\r\n altitude_variation: float = 3,\r\n seed: int = 42,\r\n name: str = \"CloudLayer\"\r\n) -> list:\r\n \"\"\"\r\n Create a layer of clouds.\r\n \r\n Args:\r\n cloud_count: Number of clouds\r\n area_size: XY area to cover\r\n altitude: Base altitude\r\n altitude_variation: Height variation\r\n seed: Random seed\r\n name: Layer name\r\n \r\n Returns:\r\n List of cloud objects\r\n \"\"\"\r\n random.seed(seed)\r\n clouds = []\r\n \r\n for i in range(cloud_count):\r\n pos = (\r\n random.uniform(-area_size[0]/2, area_size[0]/2),\r\n random.uniform(-area_size[1]/2, area_size[1]/2),\r\n altitude + random.uniform(-altitude_variation, altitude_variation)\r\n )\r\n \r\n size = (\r\n random.uniform(3, 8),\r\n random.uniform(2, 5),\r\n random.uniform(1.5, 3)\r\n )\r\n \r\n cloud = create_cloud(\r\n size=size,\r\n density=random.uniform(0.3, 0.7),\r\n location=pos,\r\n name=f\"{name}_{i+1}\"\r\n )\r\n clouds.append(cloud)\r\n \r\n return clouds\r\n\r\n\r\ndef create_stylized_cloud(\r\n size: float = 2.0,\r\n puff_count: int = 5,\r\n color: tuple = (1, 1, 1),\r\n location: tuple = (0, 0, 5),\r\n name: str = \"StylizedCloud\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a cartoon-style cloud from spheres.\r\n \r\n Args:\r\n size: Overall cloud size\r\n puff_count: Number of puffs\r\n color: Cloud color\r\n location: Position\r\n name: Object name\r\n \"\"\"\r\n puffs = []\r\n \r\n # Main puff\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=size * 0.4,\r\n location=location\r\n )\r\n main_puff = bpy.context.active_object\r\n puffs.append(main_puff)\r\n \r\n # Surrounding puffs\r\n for i in range(puff_count):\r\n angle = (i / puff_count) * 2 * math.pi\r\n dist = size * 0.3\r\n puff_size = size * random.uniform(0.25, 0.35)\r\n \r\n pos = (\r\n location[0] + math.cos(angle) * dist,\r\n location[1] + math.sin(angle) * dist * 0.5,\r\n location[2] + random.uniform(-size * 0.1, size * 0.15)\r\n )\r\n \r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=puff_size,\r\n location=pos\r\n )\r\n puffs.append(bpy.context.active_object)\r\n \r\n # Join all\r\n bpy.ops.object.select_all(action='DESELECT')\r\n for p in puffs:\r\n p.select_set(True)\r\n bpy.context.view_layer.objects.active = puffs[0]\r\n bpy.ops.object.join()\r\n \r\n cloud = bpy.context.active_object\r\n cloud.name = name\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Simple white material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n cloud.data.materials.append(mat)\r\n \r\n return cloud\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_cloud(location=(0, 0, 10))\r\n create_stylized_cloud(location=(10, 0, 8))\r\n \r\n bpy.context.scene.render.engine = 'CYCLES'\r\n print(\"Created clouds\")","category":"tasks/environment","source_file":"tasks\\environment\\cloud_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Grass and Foliage Generator. Creates grass and ground foliage using particle systems or geometry nodes.","output":"import bpy\r\nimport random\r\nimport math\r\n\r\n\r\ndef create_grass_blade(\r\n height: float = 0.1,\r\n width: float = 0.01,\r\n bend: float = 0.3,\r\n name: str = \"GrassBlade\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a single grass blade mesh.\"\"\"\r\n # Create plane for blade\r\n bpy.ops.mesh.primitive_plane_add(size=1)\r\n blade = bpy.context.active_object\r\n blade.name = name\r\n \r\n # Scale to blade shape\r\n blade.scale = (width, height, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Rotate to stand up\r\n blade.rotation_euler.x = math.radians(90)\r\n bpy.ops.object.transform_apply(rotation=True)\r\n \r\n # Add bend\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n blade.modifiers[\"SimpleDeform\"].deform_method = 'BEND'\r\n blade.modifiers[\"SimpleDeform\"].angle = bend\r\n blade.modifiers[\"SimpleDeform\"].deform_axis = 'X'\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.15, 0.4, 0.08, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n bsdf.inputs['Subsurface Weight'].default_value = 0.3\r\n blade.data.materials.append(mat)\r\n \r\n # Move to origin\r\n blade.location = (0, 0, height/2)\r\n bpy.ops.object.origin_set(type='ORIGIN_CURSOR')\r\n blade.location = (0, 0, 0)\r\n \r\n return blade\r\n\r\n\r\ndef create_grass_particles(\r\n ground: bpy.types.Object,\r\n density: int = 5000,\r\n blade_height: float = 0.1,\r\n height_random: float = 0.5,\r\n blade: bpy.types.Object = None,\r\n name: str = \"GrassSystem\"\r\n) -> tuple:\r\n \"\"\"\r\n Create grass using particle system.\r\n \r\n Args:\r\n ground: Surface to grow grass on\r\n density: Number of grass blades\r\n blade_height: Base blade height\r\n height_random: Height variation\r\n blade: Custom blade object (creates one if None)\r\n name: System name\r\n \r\n Returns:\r\n Tuple of (particle_system, blade_object)\r\n \"\"\"\r\n # Create blade if not provided\r\n if blade is None:\r\n blade = create_grass_blade(height=blade_height)\r\n blade.hide_viewport = True\r\n blade.hide_render = True\r\n \r\n bpy.context.view_layer.objects.active = ground\r\n bpy.ops.object.particle_system_add()\r\n \r\n ps = ground.particle_systems[-1]\r\n ps.name = name\r\n settings = ps.settings\r\n settings.name = f\"{name}_Settings\"\r\n \r\n # Hair type for static grass\r\n settings.type = 'HAIR'\r\n settings.count = density\r\n settings.hair_length = blade_height\r\n settings.render_type = 'OBJECT'\r\n settings.instance_object = blade\r\n settings.particle_size = 1.0\r\n settings.size_random = height_random\r\n settings.use_rotation_instance = True\r\n settings.rotation_factor_random = 0.1\r\n settings.phase_factor_random = 1.0\r\n \r\n return ps, blade\r\n\r\n\r\ndef create_flower(\r\n petals: int = 5,\r\n petal_size: float = 0.02,\r\n color: tuple = (1.0, 0.3, 0.4),\r\n stem_height: float = 0.15,\r\n name: str = \"Flower\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a simple flower.\"\"\"\r\n # Center\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=petal_size * 0.3,\r\n location=(0, 0, stem_height)\r\n )\r\n center = bpy.context.active_object\r\n \r\n # Petals\r\n for i in range(petals):\r\n angle = (i / petals) * 2 * math.pi\r\n \r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=petal_size,\r\n location=(\r\n math.cos(angle) * petal_size,\r\n math.sin(angle) * petal_size,\r\n stem_height\r\n )\r\n )\r\n petal = bpy.context.active_object\r\n petal.scale.z = 0.3\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Stem\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=petal_size * 0.1,\r\n depth=stem_height,\r\n location=(0, 0, stem_height / 2)\r\n )\r\n \r\n # Select all and join\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.join()\r\n \r\n flower = bpy.context.active_object\r\n flower.name = name\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n flower.data.materials.append(mat)\r\n \r\n return flower\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create ground\r\n bpy.ops.mesh.primitive_plane_add(size=5)\r\n ground = bpy.context.active_object\r\n \r\n # Add grass\r\n ps, blade = create_grass_particles(ground, density=3000)\r\n \r\n # Create flower\r\n create_flower(location=(1, 1, 0))\r\n \r\n print(\"Created grass and flower\")","category":"tasks/environment","source_file":"tasks\\environment\\grass_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Ground Plane Generator. Creates ground planes with various materials and textures.","output":"import bpy\r\n\r\n\r\ndef create_ground_plane(\r\n size: float = 10.0,\r\n material_type: str = 'CONCRETE',\r\n subdivisions: int = 0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Ground\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a ground plane with material.\r\n \r\n Args:\r\n size: Plane size\r\n material_type: 'CONCRETE', 'GRASS', 'DIRT', 'WOOD', 'TILE'\r\n subdivisions: Subdivision level (for displacement)\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The ground plane object\r\n \"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=location)\r\n ground = bpy.context.active_object\r\n ground.name = name\r\n \r\n if subdivisions > 0:\r\n bpy.ops.object.modifier_add(type='SUBSURF')\r\n ground.modifiers[\"Subdivision\"].levels = subdivisions\r\n ground.modifiers[\"Subdivision\"].render_levels = subdivisions\r\n \r\n # Create material based on type\r\n mat = _create_ground_material(material_type, name)\r\n ground.data.materials.append(mat)\r\n \r\n return ground\r\n\r\n\r\ndef _create_ground_material(material_type: str, name: str) -> bpy.types.Material:\r\n \"\"\"Create ground material based on type.\"\"\"\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n if material_type == 'CONCRETE':\r\n bsdf.inputs['Base Color'].default_value = (0.45, 0.45, 0.42, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n \r\n # Add noise for texture\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.inputs['Scale'].default_value = 50.0\r\n \r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.color_ramp.elements[0].color = (0.4, 0.4, 0.38, 1.0)\r\n ramp.color_ramp.elements[1].color = (0.5, 0.5, 0.47, 1.0)\r\n \r\n links.new(noise.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n elif material_type == 'GRASS':\r\n bsdf.inputs['Base Color'].default_value = (0.15, 0.4, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n bsdf.inputs['Sheen Weight'].default_value = 0.2\r\n \r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.inputs['Scale'].default_value = 30.0\r\n \r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.color_ramp.elements[0].color = (0.12, 0.35, 0.08, 1.0)\r\n ramp.color_ramp.elements[1].color = (0.2, 0.5, 0.15, 1.0)\r\n \r\n links.new(noise.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n elif material_type == 'DIRT':\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.25, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.95\r\n \r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.inputs['Scale'].default_value = 20.0\r\n \r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.color_ramp.elements[0].color = (0.3, 0.2, 0.12, 1.0)\r\n ramp.color_ramp.elements[1].color = (0.4, 0.3, 0.18, 1.0)\r\n \r\n links.new(noise.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n elif material_type == 'WOOD':\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.28, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n \r\n wave = nodes.new('ShaderNodeTexWave')\r\n wave.wave_type = 'BANDS'\r\n wave.inputs['Scale'].default_value = 8.0\r\n wave.inputs['Distortion'].default_value = 5.0\r\n \r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.color_ramp.elements[0].color = (0.35, 0.22, 0.1, 1.0)\r\n ramp.color_ramp.elements[1].color = (0.45, 0.32, 0.18, 1.0)\r\n \r\n links.new(wave.outputs['Fac'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n elif material_type == 'TILE':\r\n bsdf.inputs['Base Color'].default_value = (0.85, 0.85, 0.82, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.3\r\n \r\n brick = nodes.new('ShaderNodeTexBrick')\r\n brick.inputs['Scale'].default_value = 5.0\r\n brick.inputs['Color1'].default_value = (0.85, 0.85, 0.82, 1.0)\r\n brick.inputs['Color2'].default_value = (0.8, 0.8, 0.78, 1.0)\r\n brick.inputs['Mortar'].default_value = (0.5, 0.5, 0.48, 1.0)\r\n brick.inputs['Mortar Size'].default_value = 0.01\r\n \r\n links.new(brick.outputs['Color'], bsdf.inputs['Base Color'])\r\n \r\n return mat\r\n\r\n\r\ndef create_curved_backdrop(\r\n width: float = 10.0,\r\n height: float = 6.0,\r\n curve_radius: float = 2.0,\r\n color: tuple = (0.9, 0.9, 0.9),\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Backdrop\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create curved studio backdrop.\r\n \r\n Args:\r\n width: Backdrop width\r\n height: Backdrop height\r\n curve_radius: Curve at bottom\r\n color: Background color\r\n location: Position\r\n name: Object name\r\n \"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=1, location=location)\r\n backdrop = bpy.context.active_object\r\n backdrop.name = name\r\n backdrop.scale = (width/2, height/2, 1)\r\n backdrop.rotation_euler.x = 1.5708\r\n bpy.ops.object.transform_apply(scale=True, rotation=True)\r\n \r\n # Subdivide for bending\r\n bpy.ops.object.modifier_add(type='SUBSURF')\r\n backdrop.modifiers[\"Subdivision\"].levels = 4\r\n bpy.ops.object.modifier_apply(modifier=\"Subdivision\")\r\n \r\n # Bend bottom\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n backdrop.modifiers[\"SimpleDeform\"].deform_method = 'BEND'\r\n backdrop.modifiers[\"SimpleDeform\"].angle = 1.5708\r\n backdrop.modifiers[\"SimpleDeform\"].deform_axis = 'X'\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n backdrop.data.materials.append(mat)\r\n \r\n return backdrop\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_ground_plane(material_type='GRASS', location=(0, 0, 0))\r\n create_ground_plane(material_type='TILE', size=5, location=(0, 8, 0))\r\n \r\n print(\"Created ground planes\")","category":"tasks/environment","source_file":"tasks\\environment\\ground_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Mushroom Generator. Generates mushrooms and fungi for environment props.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_mushroom(\r\n cap_radius: float = 0.05,\r\n stem_height: float = 0.08,\r\n style: str = 'TOADSTOOL',\r\n cap_color: tuple = (0.8, 0.2, 0.1),\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Mushroom\"\r\n) -> dict:\r\n \"\"\"\r\n Create a mushroom.\r\n \r\n Args:\r\n cap_radius: Cap radius\r\n stem_height: Stem height\r\n style: 'TOADSTOOL', 'FLAT', 'FANTASY'\r\n cap_color: RGB cap color\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with mushroom parts\r\n \"\"\"\r\n result = {}\r\n \r\n # === STEM ===\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=cap_radius * 0.25,\r\n depth=stem_height,\r\n location=(location[0], location[1], location[2] + stem_height/2)\r\n )\r\n stem = bpy.context.active_object\r\n stem.name = f\"{name}_Stem\"\r\n \r\n # Slightly wider at base\r\n for v in stem.data.vertices:\r\n if v.co.z < 0:\r\n v.co.x *= 1.3\r\n v.co.y *= 1.3\r\n \r\n stem_mat = bpy.data.materials.new(f\"{name}_StemMat\")\r\n bsdf = stem_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.9, 0.88, 0.8, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n stem.data.materials.append(stem_mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n result['stem'] = stem\r\n \r\n # === CAP ===\r\n if style == 'TOADSTOOL':\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=cap_radius,\r\n location=(location[0], location[1], location[2] + stem_height)\r\n )\r\n cap = bpy.context.active_object\r\n cap.scale.z = 0.5\r\n \r\n elif style == 'FLAT':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=cap_radius,\r\n depth=cap_radius * 0.3,\r\n location=(location[0], location[1], location[2] + stem_height)\r\n )\r\n cap = bpy.context.active_object\r\n \r\n else: # FANTASY\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=cap_radius,\r\n location=(location[0], location[1], location[2] + stem_height)\r\n )\r\n cap = bpy.context.active_object\r\n cap.scale.z = 0.8\r\n \r\n bpy.ops.object.transform_apply(scale=True)\r\n cap.name = f\"{name}_Cap\"\r\n \r\n cap_mat = bpy.data.materials.new(f\"{name}_CapMat\")\r\n bsdf = cap_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*cap_color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n \r\n if style == 'FANTASY':\r\n bsdf.inputs['Emission Color'].default_value = (*cap_color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 1.0\r\n \r\n cap.data.materials.append(cap_mat)\r\n bpy.ops.object.shade_smooth()\r\n result['cap'] = cap\r\n \r\n # Spots for toadstool\r\n if style == 'TOADSTOOL':\r\n spots = _create_mushroom_spots(cap_radius, stem_height, location, name)\r\n result['spots'] = spots\r\n \r\n return result\r\n\r\n\r\ndef _create_mushroom_spots(\r\n cap_radius: float,\r\n stem_height: float,\r\n location: tuple,\r\n name: str\r\n) -> list:\r\n \"\"\"Create white spots on toadstool cap.\"\"\"\r\n spots = []\r\n spot_mat = bpy.data.materials.new(f\"{name}_SpotMat\")\r\n bsdf = spot_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.95, 0.9, 1.0)\r\n \r\n for i in range(5):\r\n angle = random.uniform(0, 2 * math.pi)\r\n dist = random.uniform(0.3, 0.7) * cap_radius\r\n \r\n spot_pos = (\r\n location[0] + math.cos(angle) * dist,\r\n location[1] + math.sin(angle) * dist,\r\n location[2] + stem_height + cap_radius * 0.3\r\n )\r\n \r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=cap_radius * 0.1,\r\n location=spot_pos\r\n )\r\n spot = bpy.context.active_object\r\n spot.name = f\"{name}_Spot_{i}\"\r\n spot.scale.z = 0.3\r\n spot.data.materials.append(spot_mat)\r\n spots.append(spot)\r\n \r\n return spots\r\n\r\n\r\ndef create_mushroom_cluster(\r\n count: int = 5,\r\n spread: float = 0.15,\r\n location: tuple = (0, 0, 0),\r\n seed: int = 42,\r\n name: str = \"MushroomCluster\"\r\n) -> list:\r\n \"\"\"Create a cluster of mushrooms.\"\"\"\r\n random.seed(seed)\r\n mushrooms = []\r\n \r\n styles = ['TOADSTOOL', 'FLAT', 'FANTASY']\r\n colors = [\r\n (0.8, 0.2, 0.1),\r\n (0.6, 0.5, 0.3),\r\n (0.4, 0.3, 0.6)\r\n ]\r\n \r\n for i in range(count):\r\n angle = random.uniform(0, 2 * math.pi)\r\n dist = random.uniform(0, spread)\r\n \r\n pos = (\r\n location[0] + math.cos(angle) * dist,\r\n location[1] + math.sin(angle) * dist,\r\n location[2]\r\n )\r\n \r\n size = random.uniform(0.03, 0.08)\r\n \r\n mushroom = create_mushroom(\r\n cap_radius=size,\r\n stem_height=size * random.uniform(1.0, 1.5),\r\n style=random.choice(styles),\r\n cap_color=random.choice(colors),\r\n location=pos,\r\n name=f\"{name}_{i+1}\"\r\n )\r\n mushrooms.append(mushroom)\r\n \r\n return mushrooms\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_mushroom(style='TOADSTOOL', location=(0, 0, 0))\r\n create_mushroom(style='FLAT', location=(0.15, 0, 0))\r\n create_mushroom_cluster(count=7, location=(0, 0.3, 0))\r\n \r\n print(\"Created mushrooms\")","category":"tasks/environment","source_file":"tasks\\environment\\mushroom_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Signpost Generator. Generates directional signposts for environments.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_signpost(\r\n height: float = 2.0,\r\n sign_count: int = 3,\r\n style: str = 'WOODEN',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Signpost\"\r\n) -> dict:\r\n \"\"\"\r\n Create a directional signpost.\r\n \r\n Args:\r\n height: Post height\r\n sign_count: Number of direction signs\r\n style: 'WOODEN', 'METAL', 'RUSTIC'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with signpost parts\r\n \"\"\"\r\n result = {}\r\n \r\n # === POST ===\r\n if style == 'WOODEN':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.05,\r\n depth=height,\r\n location=(location[0], location[1], location[2] + height/2)\r\n )\r\n else:\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0], location[1], location[2] + height/2\r\n ))\r\n bpy.context.active_object.scale = (0.04, 0.04, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n post = bpy.context.active_object\r\n post.name = f\"{name}_Post\"\r\n \r\n post_mat = bpy.data.materials.new(f\"{name}_PostMat\")\r\n bsdf = post_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n if style == 'METAL':\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.3, 0.32, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n else:\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.25, 0.12, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n \r\n post.data.materials.append(post_mat)\r\n result['post'] = post\r\n \r\n # === SIGNS ===\r\n signs = []\r\n sign_mat = bpy.data.materials.new(f\"{name}_SignMat\")\r\n bsdf = sign_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.5, 0.4, 0.25, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n \r\n for i in range(sign_count):\r\n sign_height = height * 0.6 + (i / sign_count) * height * 0.3\r\n angle = (i / sign_count) * math.pi * 0.8 - 0.4\r\n \r\n sign = _create_direction_sign(\r\n (location[0], location[1], location[2] + sign_height),\r\n angle,\r\n name,\r\n i\r\n )\r\n sign.data.materials.append(sign_mat)\r\n signs.append(sign)\r\n \r\n result['signs'] = signs\r\n \r\n return result\r\n\r\n\r\ndef _create_direction_sign(\r\n location: tuple,\r\n angle: float,\r\n name: str,\r\n index: int\r\n) -> bpy.types.Object:\r\n \"\"\"Create arrow-shaped direction sign.\"\"\"\r\n sign_length = random.uniform(0.4, 0.6)\r\n sign_height = 0.12\r\n \r\n # Create plane\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + math.cos(angle) * sign_length/2,\r\n location[1] + math.sin(angle) * sign_length/2,\r\n location[2]\r\n ))\r\n sign = bpy.context.active_object\r\n sign.name = f\"{name}_Sign_{index}\"\r\n sign.scale = (sign_length/2, 0.01, sign_height/2)\r\n sign.rotation_euler.z = angle\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Make arrow point\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n for v in sign.data.vertices:\r\n local_x = v.co.x * math.cos(-angle) - v.co.y * math.sin(-angle)\r\n if local_x > sign_length * 0.3:\r\n factor = (local_x - sign_length * 0.3) / (sign_length * 0.2)\r\n v.co.z *= 1 - factor * 0.8\r\n \r\n return sign\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_signpost(sign_count=3, location=(0, 0, 0))\r\n create_signpost(style='METAL', sign_count=2, location=(1, 0, 0))\r\n \r\n print(\"Created signposts\")","category":"tasks/environment","source_file":"tasks\\environment\\signpost_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Street Lamp Generator. Generates street lamps and light posts.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_street_lamp(\r\n height: float = 3.0,\r\n style: str = 'MODERN',\r\n lit: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"StreetLamp\"\r\n) -> dict:\r\n \"\"\"\r\n Create a street lamp.\r\n \r\n Args:\r\n height: Lamp height\r\n style: 'MODERN', 'CLASSIC', 'INDUSTRIAL'\r\n lit: Turn light on\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with lamp parts\r\n \"\"\"\r\n result = {}\r\n \r\n # === POLE ===\r\n if style == 'MODERN':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.05,\r\n depth=height,\r\n location=(location[0], location[1], location[2] + height/2)\r\n )\r\n elif style == 'CLASSIC':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.08,\r\n depth=height,\r\n location=(location[0], location[1], location[2] + height/2)\r\n )\r\n else: # INDUSTRIAL\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0], location[1], location[2] + height/2\r\n ))\r\n bpy.context.active_object.scale = (0.05, 0.05, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n pole = bpy.context.active_object\r\n pole.name = f\"{name}_Pole\"\r\n \r\n pole_mat = bpy.data.materials.new(f\"{name}_PoleMat\")\r\n bsdf = pole_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.15, 0.15, 0.17, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n pole.data.materials.append(pole_mat)\r\n \r\n result['pole'] = pole\r\n \r\n # === LAMP HEAD ===\r\n head = _create_lamp_head(height, style, location, name, lit)\r\n result.update(head)\r\n \r\n # === BASE ===\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.15,\r\n depth=0.05,\r\n location=(location[0], location[1], location[2] + 0.025)\r\n )\r\n base = bpy.context.active_object\r\n base.name = f\"{name}_Base\"\r\n base.data.materials.append(pole_mat)\r\n result['base'] = base\r\n \r\n return result\r\n\r\n\r\ndef _create_lamp_head(\r\n height: float,\r\n style: str,\r\n location: tuple,\r\n name: str,\r\n lit: bool\r\n) -> dict:\r\n \"\"\"Create lamp head based on style.\"\"\"\r\n result = {}\r\n head_z = location[2] + height\r\n \r\n if style == 'MODERN':\r\n # Arm\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.03,\r\n depth=0.5,\r\n location=(location[0] + 0.25, location[1], head_z)\r\n )\r\n arm = bpy.context.active_object\r\n arm.rotation_euler.y = math.radians(90)\r\n arm.name = f\"{name}_Arm\"\r\n result['arm'] = arm\r\n \r\n # Head box\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + 0.4, location[1], head_z - 0.1\r\n ))\r\n head = bpy.context.active_object\r\n head.scale = (0.15, 0.08, 0.05)\r\n head.name = f\"{name}_Head\"\r\n result['head'] = head\r\n \r\n light_pos = (location[0] + 0.4, location[1], head_z - 0.15)\r\n \r\n elif style == 'CLASSIC':\r\n # Ornate lantern\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.15,\r\n depth=0.3,\r\n location=(location[0], location[1], head_z + 0.1)\r\n )\r\n head = bpy.context.active_object\r\n head.name = f\"{name}_Lantern\"\r\n result['head'] = head\r\n \r\n light_pos = (location[0], location[1], head_z)\r\n \r\n else: # INDUSTRIAL\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=0.2,\r\n radius2=0.05,\r\n depth=0.15,\r\n location=(location[0], location[1], head_z)\r\n )\r\n head = bpy.context.active_object\r\n head.name = f\"{name}_Shade\"\r\n result['head'] = head\r\n \r\n light_pos = (location[0], location[1], head_z - 0.05)\r\n \r\n # Light source\r\n if lit:\r\n bpy.ops.object.light_add(type='POINT', location=light_pos)\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 500\r\n light.data.color = (1.0, 0.9, 0.7)\r\n light.data.shadow_soft_size = 0.1\r\n result['light'] = light\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_street_lamp(style='MODERN', location=(0, 0, 0))\r\n create_street_lamp(style='CLASSIC', location=(2, 0, 0))\r\n create_street_lamp(style='INDUSTRIAL', location=(4, 0, 0))\r\n \r\n print(\"Created street lamps\")","category":"tasks/environment","source_file":"tasks\\environment\\street_lamp_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Terrain Scatter System. Creates terrain with scattered objects (trees, rocks, grass) using particle systems or geometry nodes.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_simple_terrain(\r\n size: float = 50.0,\r\n subdivisions: int = 100,\r\n noise_strength: float = 3.0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Terrain\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a terrain mesh with procedural displacement.\r\n \r\n Args:\r\n size: Terrain size in meters\r\n subdivisions: Mesh resolution\r\n noise_strength: Height variation amount\r\n location: Terrain position\r\n name: Object name\r\n \r\n Returns:\r\n The created terrain object\r\n \r\n Example:\r\n >>> terrain = create_simple_terrain(size=100, noise_strength=5)\r\n \"\"\"\r\n # Create base plane\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=location)\r\n terrain = bpy.context.active_object\r\n terrain.name = name\r\n \r\n # Subdivide for detail\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.subdivide(number_cuts=subdivisions)\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n # Add displacement modifier\r\n bpy.ops.object.modifier_add(type='DISPLACE')\r\n displace = terrain.modifiers[\"Displace\"]\r\n \r\n # Create noise texture\r\n tex = bpy.data.textures.new(f\"{name}_Noise\", type='CLOUDS')\r\n tex.noise_scale = 2.0\r\n tex.noise_depth = 3\r\n \r\n displace.texture = tex\r\n displace.strength = noise_strength\r\n displace.mid_level = 0.5\r\n \r\n # Apply modifier\r\n bpy.ops.object.modifier_apply(modifier=\"Displace\")\r\n \r\n # Smooth shading\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Add terrain material\r\n terrain_mat = bpy.data.materials.new(name=f\"{name}_Material\")\r\n bsdf = terrain_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.2, 0.35, 0.1, 1.0) # Green\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n terrain.data.materials.append(terrain_mat)\r\n \r\n return terrain\r\n\r\n\r\ndef create_scatter_object(\r\n scatter_type: str = 'TREE',\r\n name: str = None\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a simple object for scattering (tree, rock, grass).\r\n \r\n Args:\r\n scatter_type: 'TREE', 'ROCK', 'GRASS', 'BUSH'\r\n name: Custom name (auto-generated if None)\r\n \r\n Returns:\r\n Created object\r\n \"\"\"\r\n obj_name = name or f\"Scatter_{scatter_type}\"\r\n \r\n if scatter_type.upper() == 'TREE':\r\n # Simple tree: cone + cylinder\r\n bpy.ops.mesh.primitive_cone_add(radius1=0.8, radius2=0, depth=2, location=(0, 0, 2.5))\r\n foliage = bpy.context.active_object\r\n foliage.name = f\"{obj_name}_Foliage\"\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(radius=0.15, depth=1.5, location=(0, 0, 0.75))\r\n trunk = bpy.context.active_object\r\n trunk.name = f\"{obj_name}_Trunk\"\r\n \r\n # Materials\r\n foliage_mat = bpy.data.materials.new(name=f\"{obj_name}_FoliageMat\")\r\n foliage_mat.node_tree.nodes[\"Principled BSDF\"].inputs['Base Color'].default_value = (0.1, 0.4, 0.1, 1.0)\r\n foliage.data.materials.append(foliage_mat)\r\n \r\n trunk_mat = bpy.data.materials.new(name=f\"{obj_name}_TrunkMat\")\r\n trunk_mat.node_tree.nodes[\"Principled BSDF\"].inputs['Base Color'].default_value = (0.3, 0.2, 0.1, 1.0)\r\n trunk.data.materials.append(trunk_mat)\r\n \r\n # Join into single object\r\n foliage.select_set(True)\r\n trunk.select_set(True)\r\n bpy.context.view_layer.objects.active = foliage\r\n bpy.ops.object.join()\r\n foliage.name = obj_name\r\n return foliage\r\n \r\n elif scatter_type.upper() == 'ROCK':\r\n bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.5, location=(0, 0, 0.25))\r\n rock = bpy.context.active_object\r\n rock.name = obj_name\r\n rock.scale = (1.0, 0.8, 0.6)\r\n \r\n # Randomize\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.vertices_smooth(factor=0.5)\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n rock_mat = bpy.data.materials.new(name=f\"{obj_name}_Mat\")\r\n rock_mat.node_tree.nodes[\"Principled BSDF\"].inputs['Base Color'].default_value = (0.4, 0.4, 0.4, 1.0)\r\n rock_mat.node_tree.nodes[\"Principled BSDF\"].inputs['Roughness'].default_value = 0.9\r\n rock.data.materials.append(rock_mat)\r\n return rock\r\n \r\n elif scatter_type.upper() == 'GRASS':\r\n bpy.ops.mesh.primitive_cone_add(radius1=0.02, radius2=0, depth=0.3, location=(0, 0, 0.15))\r\n grass = bpy.context.active_object\r\n grass.name = obj_name\r\n \r\n grass_mat = bpy.data.materials.new(name=f\"{obj_name}_Mat\")\r\n grass_mat.node_tree.nodes[\"Principled BSDF\"].inputs['Base Color'].default_value = (0.15, 0.5, 0.1, 1.0)\r\n grass.data.materials.append(grass_mat)\r\n return grass\r\n \r\n return None\r\n\r\n\r\ndef scatter_on_surface(\r\n surface: bpy.types.Object,\r\n scatter_object: bpy.types.Object,\r\n count: int = 100,\r\n seed: int = 0,\r\n scale_min: float = 0.8,\r\n scale_max: float = 1.2,\r\n rotation_random: float = 1.0,\r\n name: str = \"ScatterSystem\"\r\n) -> bpy.types.ParticleSystem:\r\n \"\"\"\r\n Scatter objects on a surface using particle system.\r\n \r\n Args:\r\n surface: Target surface for scattering\r\n scatter_object: Object to scatter (will be instanced)\r\n count: Number of instances\r\n seed: Random seed for reproducibility\r\n scale_min: Minimum scale multiplier\r\n scale_max: Maximum scale multiplier\r\n rotation_random: Rotation randomness factor\r\n name: Particle system name\r\n \r\n Returns:\r\n The created particle system\r\n \r\n Example:\r\n >>> tree = create_scatter_object('TREE')\r\n >>> scatter_on_surface(terrain, tree, count=50)\r\n \"\"\"\r\n # Add particle system\r\n bpy.context.view_layer.objects.active = surface\r\n bpy.ops.object.particle_system_add()\r\n \r\n particle_sys = surface.particle_systems[-1]\r\n particle_sys.name = name\r\n settings = particle_sys.settings\r\n settings.name = f\"{name}_Settings\"\r\n \r\n # Configure as hair (static particles)\r\n settings.type = 'HAIR'\r\n settings.use_advanced_hair = True\r\n \r\n # Count and distribution\r\n settings.count = count\r\n settings.hair_length = 1.0\r\n settings.emit_from = 'FACE'\r\n settings.use_modifier_stack = True\r\n \r\n # Random seed\r\n settings.use_emit_random = True\r\n particle_sys.seed = seed\r\n \r\n # Render as object\r\n settings.render_type = 'OBJECT'\r\n settings.instance_object = scatter_object\r\n settings.use_rotation_instance = True\r\n settings.use_scale_instance = True\r\n \r\n # Random scale\r\n settings.particle_size = (scale_min + scale_max) / 2\r\n settings.size_random = (scale_max - scale_min) / (scale_min + scale_max)\r\n \r\n # Random rotation\r\n settings.rotation_mode = 'OB_Z'\r\n settings.rotation_factor_random = rotation_random\r\n settings.phase_factor_random = 2.0\r\n \r\n # Hide original object\r\n scatter_object.hide_viewport = True\r\n scatter_object.hide_render = True\r\n \r\n return particle_sys\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create terrain\r\n terrain = create_simple_terrain(size=30, noise_strength=2)\r\n \r\n # Create and scatter trees\r\n tree = create_scatter_object('TREE', 'PineTree')\r\n scatter_on_surface(terrain, tree, count=30, seed=42)\r\n \r\n # Create and scatter rocks\r\n rock = create_scatter_object('ROCK', 'Boulder')\r\n scatter_on_surface(terrain, rock, count=20, seed=123, scale_min=0.5, scale_max=2.0)\r\n \r\n print(\"Terrain with scattered objects created!\")","category":"tasks/environment","source_file":"tasks\\environment\\terrain_scatter.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Tree Generator. Procedurally generates different types of trees with trunks, branches, and foliage.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_tree(\r\n tree_type: str = 'OAK',\r\n height: float = 5.0,\r\n trunk_radius: float = 0.2,\r\n location: tuple = (0, 0, 0),\r\n seed: int = 42,\r\n foliage_density: float = 1.0,\r\n name_prefix: str = \"Tree\"\r\n) -> dict:\r\n \"\"\"\r\n Create a procedural tree.\r\n \r\n Args:\r\n tree_type: 'OAK', 'PINE', 'PALM', 'BIRCH', 'WILLOW'\r\n height: Total tree height\r\n trunk_radius: Base trunk radius\r\n location: Tree base position\r\n seed: Random seed for variation\r\n foliage_density: Foliage amount multiplier\r\n name_prefix: Prefix for object names\r\n \r\n Returns:\r\n Dictionary with tree objects\r\n \r\n Example:\r\n >>> tree = create_tree('PINE', height=8, location=(5, 0, 0))\r\n \"\"\"\r\n random.seed(seed)\r\n result = {}\r\n \r\n tree_configs = {\r\n 'OAK': {\r\n 'trunk_height': 0.35,\r\n 'crown_shape': 'SPHERE',\r\n 'crown_scale': (1.0, 1.0, 0.8),\r\n 'branch_count': 5,\r\n 'trunk_color': (0.25, 0.15, 0.08),\r\n 'foliage_color': (0.15, 0.4, 0.12)\r\n },\r\n 'PINE': {\r\n 'trunk_height': 0.9,\r\n 'crown_shape': 'CONE',\r\n 'crown_scale': (0.6, 0.6, 1.5),\r\n 'branch_count': 0,\r\n 'trunk_color': (0.35, 0.2, 0.1),\r\n 'foliage_color': (0.08, 0.25, 0.08)\r\n },\r\n 'PALM': {\r\n 'trunk_height': 0.85,\r\n 'crown_shape': 'FRONDS',\r\n 'crown_scale': (1.0, 1.0, 0.3),\r\n 'branch_count': 0,\r\n 'trunk_color': (0.4, 0.3, 0.2),\r\n 'foliage_color': (0.12, 0.35, 0.08)\r\n },\r\n 'BIRCH': {\r\n 'trunk_height': 0.4,\r\n 'crown_shape': 'ELLIPSOID',\r\n 'crown_scale': (0.7, 0.7, 1.2),\r\n 'branch_count': 4,\r\n 'trunk_color': (0.9, 0.88, 0.85),\r\n 'foliage_color': (0.2, 0.45, 0.1)\r\n },\r\n 'WILLOW': {\r\n 'trunk_height': 0.3,\r\n 'crown_shape': 'HANGING',\r\n 'crown_scale': (1.5, 1.5, 1.0),\r\n 'branch_count': 6,\r\n 'trunk_color': (0.3, 0.22, 0.12),\r\n 'foliage_color': (0.18, 0.4, 0.15)\r\n }\r\n }\r\n \r\n config = tree_configs.get(tree_type, tree_configs['OAK'])\r\n \r\n # Create trunk\r\n trunk = _create_trunk(height, trunk_radius, config, location, name_prefix)\r\n result['trunk'] = trunk\r\n \r\n # Create crown/foliage\r\n crown_height = height * config['trunk_height']\r\n crown = _create_crown(\r\n height, crown_height, config, location, \r\n foliage_density, name_prefix\r\n )\r\n result['crown'] = crown\r\n \r\n # Create branches for certain tree types\r\n if config['branch_count'] > 0:\r\n branches = _create_branches(\r\n height, trunk_radius, config, location, name_prefix\r\n )\r\n result['branches'] = branches\r\n \r\n return result\r\n\r\n\r\ndef _create_trunk(\r\n height: float,\r\n radius: float,\r\n config: dict,\r\n location: tuple,\r\n name_prefix: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create tree trunk.\"\"\"\r\n trunk_height = height * config['trunk_height']\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius,\r\n depth=trunk_height,\r\n location=(location[0], location[1], location[2] + trunk_height/2)\r\n )\r\n trunk = bpy.context.active_object\r\n trunk.name = f\"{name_prefix}_Trunk\"\r\n \r\n # Taper the trunk\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n trunk.modifiers[\"SimpleDeform\"].deform_method = 'TAPER'\r\n trunk.modifiers[\"SimpleDeform\"].factor = 0.4\r\n trunk.modifiers[\"SimpleDeform\"].deform_axis = 'Z'\r\n \r\n # Add slight bend for naturalness\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n trunk.modifiers[\"SimpleDeform.001\"].deform_method = 'BEND'\r\n trunk.modifiers[\"SimpleDeform.001\"].angle = math.radians(random.uniform(-5, 5))\r\n trunk.modifiers[\"SimpleDeform.001\"].deform_axis = 'X'\r\n \r\n # Trunk material\r\n mat = bpy.data.materials.new(f\"{name_prefix}_TrunkMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*config['trunk_color'], 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n trunk.data.materials.append(mat)\r\n \r\n return trunk\r\n\r\n\r\ndef _create_crown(\r\n height: float,\r\n crown_base_height: float,\r\n config: dict,\r\n location: tuple,\r\n density: float,\r\n name_prefix: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create tree crown/foliage.\"\"\"\r\n crown_pos = (\r\n location[0],\r\n location[1],\r\n location[2] + crown_base_height + height * 0.2\r\n )\r\n \r\n shape = config['crown_shape']\r\n scale = config['crown_scale']\r\n crown_size = height * 0.4 * density\r\n \r\n if shape == 'SPHERE':\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=crown_size,\r\n subdivisions=2,\r\n location=crown_pos\r\n )\r\n elif shape == 'CONE':\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=crown_size * 0.8,\r\n radius2=0.1,\r\n depth=height * 0.6,\r\n location=(crown_pos[0], crown_pos[1], crown_pos[2] + height * 0.15)\r\n )\r\n elif shape == 'ELLIPSOID':\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=crown_size,\r\n location=crown_pos\r\n )\r\n elif shape == 'FRONDS':\r\n # Palm fronds - create multiple leaves\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=crown_size * 0.5,\r\n location=crown_pos\r\n )\r\n else: # HANGING/WILLOW\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=crown_size,\r\n location=crown_pos\r\n )\r\n \r\n crown = bpy.context.active_object\r\n crown.name = f\"{name_prefix}_Crown\"\r\n \r\n # Apply scale\r\n crown.scale = scale\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Add noise for organic shape\r\n bpy.ops.object.modifier_add(type='DISPLACE')\r\n crown.modifiers[\"Displace\"].strength = crown_size * 0.2\r\n \r\n # Create noise texture\r\n tex = bpy.data.textures.new(f\"{name_prefix}_Noise\", type='CLOUDS')\r\n tex.noise_scale = 1.5\r\n crown.modifiers[\"Displace\"].texture = tex\r\n \r\n # Foliage material\r\n mat = bpy.data.materials.new(f\"{name_prefix}_FoliageMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*config['foliage_color'], 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n bsdf.inputs['Subsurface Weight'].default_value = 0.2\r\n bsdf.inputs['Subsurface Radius'].default_value = (0.1, 0.2, 0.05)\r\n crown.data.materials.append(mat)\r\n \r\n return crown\r\n\r\n\r\ndef _create_branches(\r\n height: float,\r\n trunk_radius: float,\r\n config: dict,\r\n location: tuple,\r\n name_prefix: str\r\n) -> list:\r\n \"\"\"Create tree branches.\"\"\"\r\n branches = []\r\n count = config['branch_count']\r\n branch_height = height * config['trunk_height']\r\n \r\n for i in range(count):\r\n angle = (i / count) * 2 * math.pi + random.uniform(-0.3, 0.3)\r\n branch_z = branch_height * (0.5 + random.uniform(0, 0.4))\r\n \r\n # Branch start position\r\n start_x = location[0] + math.cos(angle) * trunk_radius * 0.8\r\n start_y = location[1] + math.sin(angle) * trunk_radius * 0.8\r\n \r\n # Branch end position\r\n branch_length = height * random.uniform(0.2, 0.4)\r\n end_x = location[0] + math.cos(angle) * (trunk_radius + branch_length)\r\n end_y = location[1] + math.sin(angle) * (trunk_radius + branch_length)\r\n end_z = branch_z + branch_length * 0.3\r\n \r\n # Create branch cylinder\r\n mid_x = (start_x + end_x) / 2\r\n mid_y = (start_y + end_y) / 2\r\n mid_z = (branch_z + end_z) / 2\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=trunk_radius * 0.2,\r\n depth=branch_length,\r\n location=(mid_x, mid_y, mid_z)\r\n )\r\n \r\n branch = bpy.context.active_object\r\n branch.name = f\"{name_prefix}_Branch_{i}\"\r\n \r\n # Rotate to point outward\r\n branch.rotation_euler.x = math.radians(-20 + random.uniform(-10, 10))\r\n branch.rotation_euler.z = angle\r\n \r\n # Use trunk material\r\n if bpy.data.materials.get(f\"{name_prefix}_TrunkMat\"):\r\n branch.data.materials.append(bpy.data.materials[f\"{name_prefix}_TrunkMat\"])\r\n \r\n branches.append(branch)\r\n \r\n return branches\r\n\r\n\r\ndef create_forest_patch(\r\n center: tuple = (0, 0, 0),\r\n radius: float = 10,\r\n tree_count: int = 10,\r\n tree_types: list = None,\r\n min_height: float = 4,\r\n max_height: float = 8,\r\n seed: int = 123\r\n) -> list:\r\n \"\"\"\r\n Create a patch of randomly placed trees.\r\n \r\n Args:\r\n center: Center of the forest area\r\n radius: Spread radius\r\n tree_count: Number of trees\r\n tree_types: List of tree types to use\r\n min_height: Minimum tree height\r\n max_height: Maximum tree height\r\n seed: Random seed\r\n \r\n Returns:\r\n List of tree dictionaries\r\n \"\"\"\r\n random.seed(seed)\r\n \r\n if tree_types is None:\r\n tree_types = ['OAK', 'PINE', 'BIRCH']\r\n \r\n trees = []\r\n \r\n for i in range(tree_count):\r\n # Random position within radius\r\n angle = random.uniform(0, 2 * math.pi)\r\n dist = random.uniform(0, radius)\r\n \r\n pos = (\r\n center[0] + math.cos(angle) * dist,\r\n center[1] + math.sin(angle) * dist,\r\n center[2]\r\n )\r\n \r\n tree_type = random.choice(tree_types)\r\n height = random.uniform(min_height, max_height)\r\n \r\n tree = create_tree(\r\n tree_type=tree_type,\r\n height=height,\r\n location=pos,\r\n seed=seed + i,\r\n name_prefix=f\"Tree_{i}\"\r\n )\r\n trees.append(tree)\r\n \r\n return trees\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create different tree types\r\n tree1 = create_tree('OAK', height=6, location=(0, 0, 0))\r\n tree2 = create_tree('PINE', height=8, location=(4, 0, 0))\r\n tree3 = create_tree('BIRCH', height=5, location=(-4, 0, 0))\r\n \r\n print(f\"Created 3 trees\")","category":"tasks/environment","source_file":"tasks\\environment\\tree_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Water Plane Generator. Creates stylized water surfaces with animated materials.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_water_plane(\r\n size: float = 10.0,\r\n style: str = 'STYLIZED',\r\n color: tuple = (0.1, 0.4, 0.6),\r\n wave_scale: float = 2.0,\r\n animated: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Water\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a water surface.\r\n \r\n Args:\r\n size: Plane size\r\n style: 'STYLIZED', 'REALISTIC', 'TOON'\r\n color: RGB water color\r\n wave_scale: Wave pattern scale\r\n animated: Add wave animation\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The water plane object\r\n \"\"\"\r\n # Create subdivided plane\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=location)\r\n water = bpy.context.active_object\r\n water.name = name\r\n \r\n # Subdivide for waves\r\n bpy.ops.object.modifier_add(type='SUBSURF')\r\n water.modifiers[\"Subdivision\"].levels = 4\r\n water.modifiers[\"Subdivision\"].render_levels = 4\r\n \r\n # Create water material\r\n mat = _create_water_material(name, style, color, wave_scale, animated)\r\n water.data.materials.append(mat)\r\n \r\n return water\r\n\r\n\r\ndef _create_water_material(\r\n name: str,\r\n style: str,\r\n color: tuple,\r\n wave_scale: float,\r\n animated: bool\r\n) -> bpy.types.Material:\r\n \"\"\"Create water shader material.\"\"\"\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (600, 0)\r\n \r\n bsdf = nodes.new('ShaderNodeBsdfPrincipled')\r\n bsdf.location = (300, 0)\r\n \r\n if style == 'STYLIZED':\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.0\r\n bsdf.inputs['Roughness'].default_value = 0.1\r\n bsdf.inputs['IOR'].default_value = 1.33\r\n bsdf.inputs['Transmission Weight'].default_value = 0.5\r\n \r\n elif style == 'REALISTIC':\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.05\r\n bsdf.inputs['IOR'].default_value = 1.33\r\n bsdf.inputs['Transmission Weight'].default_value = 0.9\r\n \r\n elif style == 'TOON':\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n bsdf.inputs['Specular IOR Level'].default_value = 0.8\r\n \r\n links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])\r\n \r\n # Wave normal\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-400, 100)\r\n \r\n noise1 = nodes.new('ShaderNodeTexNoise')\r\n noise1.location = (-200, 200)\r\n noise1.inputs['Scale'].default_value = wave_scale\r\n noise1.inputs['Detail'].default_value = 2.0\r\n \r\n noise2 = nodes.new('ShaderNodeTexNoise')\r\n noise2.location = (-200, 0)\r\n noise2.inputs['Scale'].default_value = wave_scale * 2\r\n noise2.inputs['Detail'].default_value = 4.0\r\n \r\n mix = nodes.new('ShaderNodeMix')\r\n mix.data_type = 'RGBA'\r\n mix.location = (0, 100)\r\n mix.inputs['Factor'].default_value = 0.5\r\n \r\n bump = nodes.new('ShaderNodeBump')\r\n bump.location = (150, 100)\r\n bump.inputs['Strength'].default_value = 0.3\r\n \r\n links.new(tex_coord.outputs['Generated'], noise1.inputs['Vector'])\r\n links.new(tex_coord.outputs['Generated'], noise2.inputs['Vector'])\r\n links.new(noise1.outputs['Fac'], mix.inputs[6])\r\n links.new(noise2.outputs['Fac'], mix.inputs[7])\r\n links.new(mix.outputs[2], bump.inputs['Height'])\r\n links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])\r\n \r\n # Animation driver\r\n if animated:\r\n # Add driver to noise offset\r\n driver = noise1.inputs['W'].driver_add('default_value')\r\n driver.driver.expression = 'frame / 50'\r\n \r\n driver2 = noise2.inputs['W'].driver_add('default_value')\r\n driver2.driver.expression = 'frame / 30'\r\n \r\n noise1.noise_dimensions = '4D'\r\n noise2.noise_dimensions = '4D'\r\n \r\n return mat\r\n\r\n\r\ndef create_ocean_modifier(\r\n plane: bpy.types.Object,\r\n resolution: int = 10,\r\n spatial_size: int = 50,\r\n wave_scale: float = 1.0,\r\n choppiness: float = 1.0\r\n) -> bpy.types.Modifier:\r\n \"\"\"Add ocean modifier for realistic water.\"\"\"\r\n mod = plane.modifiers.new(\"Ocean\", 'OCEAN')\r\n mod.resolution = resolution\r\n mod.spatial_size = spatial_size\r\n mod.wave_scale = wave_scale\r\n mod.choppiness = choppiness\r\n mod.use_normals = True\r\n mod.time = 1.0\r\n \r\n # Animate\r\n mod.keyframe_insert('time', frame=1)\r\n bpy.context.scene.frame_set(250)\r\n mod.time = 10.0\r\n mod.keyframe_insert('time', frame=250)\r\n bpy.context.scene.frame_set(1)\r\n \r\n return mod\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_water_plane(size=20, style='STYLIZED')\r\n \r\n print(\"Created water plane\")","category":"tasks/environment","source_file":"tasks\\environment\\water_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Well Generator. Generates medieval-style wells with rope and bucket.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_well(\r\n radius: float = 0.5,\r\n height: float = 0.6,\r\n with_roof: bool = True,\r\n with_bucket: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Well\"\r\n) -> dict:\r\n \"\"\"\r\n Create a well.\r\n \r\n Args:\r\n radius: Well radius\r\n height: Wall height\r\n with_roof: Add roof structure\r\n with_bucket: Add rope and bucket\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with well parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Stone wall\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius,\r\n depth=height,\r\n location=(location[0], location[1], location[2] + height/2)\r\n )\r\n wall = bpy.context.active_object\r\n wall.name = f\"{name}_Wall\"\r\n \r\n # Hollow out\r\n bpy.ops.object.modifier_add(type='SOLIDIFY')\r\n wall.modifiers[\"Solidify\"].thickness = -0.1\r\n \r\n wall_mat = bpy.data.materials.new(f\"{name}_StoneMat\")\r\n bsdf = wall_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.38, 0.35, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n wall.data.materials.append(wall_mat)\r\n \r\n result['wall'] = wall\r\n \r\n # Roof\r\n if with_roof:\r\n roof_parts = _create_well_roof(radius, height, location, name)\r\n result.update(roof_parts)\r\n \r\n # Bucket\r\n if with_bucket:\r\n bucket_parts = _create_well_bucket(radius, height, location, name)\r\n result.update(bucket_parts)\r\n \r\n return result\r\n\r\n\r\ndef _create_well_roof(radius, height, location, name):\r\n \"\"\"Create well roof structure.\"\"\"\r\n result = {}\r\n post_height = 1.2\r\n \r\n wood_mat = bpy.data.materials.new(f\"{name}_WoodMat\")\r\n bsdf = wood_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.25, 0.12, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n \r\n # Posts\r\n for side in [-1, 1]:\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.04,\r\n depth=post_height,\r\n location=(\r\n location[0] + side * (radius - 0.05),\r\n location[1],\r\n location[2] + height + post_height/2\r\n )\r\n )\r\n post = bpy.context.active_object\r\n post.name = f\"{name}_Post_{'L' if side < 0 else 'R'}\"\r\n post.data.materials.append(wood_mat)\r\n \r\n # Crossbeam\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.04,\r\n depth=radius * 2,\r\n location=(location[0], location[1], location[2] + height + post_height)\r\n )\r\n beam = bpy.context.active_object\r\n beam.name = f\"{name}_Beam\"\r\n beam.rotation_euler.y = 1.5708\r\n beam.data.materials.append(wood_mat)\r\n result['beam'] = beam\r\n \r\n # Roof\r\n bpy.ops.mesh.primitive_cone_add(\r\n vertices=4,\r\n radius1=radius * 1.3,\r\n radius2=0,\r\n depth=0.6,\r\n location=(location[0], location[1], location[2] + height + post_height + 0.3)\r\n )\r\n roof = bpy.context.active_object\r\n roof.name = f\"{name}_Roof\"\r\n roof.rotation_euler.z = 0.785\r\n \r\n roof_mat = bpy.data.materials.new(f\"{name}_RoofMat\")\r\n bsdf = roof_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.25, 0.18, 0.1, 1.0)\r\n roof.data.materials.append(roof_mat)\r\n result['roof'] = roof\r\n \r\n return result\r\n\r\n\r\ndef _create_well_bucket(radius, height, location, name):\r\n \"\"\"Create bucket and rope.\"\"\"\r\n result = {}\r\n \r\n # Bucket\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.08,\r\n depth=0.12,\r\n location=(location[0], location[1], location[2] + 0.2)\r\n )\r\n bucket = bpy.context.active_object\r\n bucket.name = f\"{name}_Bucket\"\r\n \r\n bpy.ops.object.modifier_add(type='SOLIDIFY')\r\n bucket.modifiers[\"Solidify\"].thickness = -0.008\r\n \r\n bucket_mat = bpy.data.materials.new(f\"{name}_BucketMat\")\r\n bsdf = bucket_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.35, 0.25, 1.0)\r\n bucket.data.materials.append(bucket_mat)\r\n result['bucket'] = bucket\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_well(location=(0, 0, 0))\r\n \r\n print(\"Created well\")","category":"tasks/environment","source_file":"tasks\\environment\\well_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: HDRI Lighting Setup. Sets up HDRI environment lighting for realistic illumination.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef setup_hdri_lighting(\r\n hdri_path: str,\r\n strength: float = 1.0,\r\n rotation: float = 0,\r\n background_strength: float = None\r\n) -> dict:\r\n \"\"\"\r\n Set up HDRI environment lighting.\r\n \r\n Args:\r\n hdri_path: Path to HDRI image file\r\n strength: Light intensity\r\n rotation: Z rotation in degrees\r\n background_strength: Separate background strength (uses strength if None)\r\n \r\n Returns:\r\n Dictionary with world and nodes\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Environment texture\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.location = (-600, 300)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n \r\n # Mapping for rotation\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-800, 300)\r\n mapping.inputs['Rotation'].default_value[2] = math.radians(rotation)\r\n \r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-1000, 300)\r\n \r\n # Background shader\r\n background = nodes.new('ShaderNodeBackground')\r\n background.location = (-200, 300)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (0, 300)\r\n \r\n # Connect\r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n \r\n return {\r\n 'world': world,\r\n 'env_texture': env_tex,\r\n 'mapping': mapping,\r\n 'background': background\r\n }\r\n\r\n\r\ndef setup_gradient_sky(\r\n sky_color: tuple = (0.4, 0.6, 0.9),\r\n horizon_color: tuple = (0.8, 0.85, 0.9),\r\n ground_color: tuple = (0.2, 0.2, 0.15),\r\n strength: float = 1.0\r\n) -> dict:\r\n \"\"\"\r\n Set up gradient sky background.\r\n \r\n Args:\r\n sky_color: RGB color at top\r\n horizon_color: RGB color at horizon\r\n ground_color: RGB color below horizon\r\n strength: Light strength\r\n \r\n Returns:\r\n Dictionary with world and nodes\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Texture coordinate\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 0)\r\n \r\n # Separate Z\r\n separate = nodes.new('ShaderNodeSeparateXYZ')\r\n separate.location = (-600, 0)\r\n \r\n # Color ramp for sky gradient\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (-400, 0)\r\n ramp.color_ramp.elements[0].position = 0.4\r\n ramp.color_ramp.elements[0].color = (*ground_color, 1.0)\r\n ramp.color_ramp.elements[1].position = 0.5\r\n ramp.color_ramp.elements[1].color = (*horizon_color, 1.0)\r\n \r\n # Add sky color stop\r\n sky_stop = ramp.color_ramp.elements.new(0.7)\r\n sky_stop.color = (*sky_color, 1.0)\r\n \r\n # Background\r\n background = nodes.new('ShaderNodeBackground')\r\n background.location = (-100, 0)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (100, 0)\r\n \r\n links.new(tex_coord.outputs['Generated'], separate.inputs['Vector'])\r\n links.new(separate.outputs['Z'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n \r\n return {'world': world, 'color_ramp': ramp}\r\n\r\n\r\ndef setup_physical_sky(\r\n sun_elevation: float = 45,\r\n sun_rotation: float = 0,\r\n turbidity: float = 2.0,\r\n strength: float = 1.0\r\n) -> dict:\r\n \"\"\"\r\n Set up physical sky simulation.\r\n \r\n Args:\r\n sun_elevation: Sun angle above horizon (degrees)\r\n sun_rotation: Sun compass direction (degrees)\r\n turbidity: Atmosphere haze (1-10)\r\n strength: Light strength\r\n \r\n Returns:\r\n Dictionary with world and sky texture\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Sky texture\r\n sky = nodes.new('ShaderNodeTexSky')\r\n sky.location = (-300, 0)\r\n sky.sky_type = 'NISHITA'\r\n sky.sun_elevation = math.radians(sun_elevation)\r\n sky.sun_rotation = math.radians(sun_rotation)\r\n sky.air_density = 1.0\r\n sky.dust_density = turbidity / 10\r\n sky.ozone_density = 1.0\r\n \r\n # Background\r\n background = nodes.new('ShaderNodeBackground')\r\n background.location = (0, 0)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n \r\n links.new(sky.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n \r\n return {'world': world, 'sky_texture': sky}\r\n\r\n\r\ndef set_world_color(color: tuple, strength: float = 1.0) -> None:\r\n \"\"\"Set solid color world background.\"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n background = nodes.new('ShaderNodeBackground')\r\n background.inputs['Color'].default_value = (*color, 1.0)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\nif __name__ == \"__main__\":\r\n # Set up a physical sky\r\n setup_physical_sky(sun_elevation=30, sun_rotation=45)\r\n print(\"Set up physical sky lighting\")","category":"tasks/lighting","source_file":"tasks\\lighting\\hdri_setup.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Set World Color. Category: tasks/lighting","output":"import bpy\n\ndef set_world_color(color: tuple, strength: float = 1.0) -> None:\r\n \"\"\"Set solid color world background.\"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n background = nodes.new('ShaderNodeBackground')\r\n background.inputs['Color'].default_value = (*color, 1.0)\r\n background.inputs['Strength'].default_value = strength\r\n \r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\nif __name__ == \"__main__\":\r\n # Set up a physical sky\r\n setup_physical_sky(sun_elevation=30, sun_rotation=45)\r\n print(\"Set up physical sky lighting\")","category":"tasks/lighting","source_file":"tasks\\lighting\\hdri_setup.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Create Outdoor Sun Lighting. Creates realistic outdoor lighting with sun, sky, and optional HDRI environment.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_outdoor_sun_lighting(\r\n sun_angle: float = 45,\r\n sun_rotation: float = 30,\r\n sun_energy: float = 5.0,\r\n sun_color: tuple = (1.0, 0.98, 0.95),\r\n sky_type: str = 'NISHITA',\r\n time_of_day: str = 'NOON',\r\n name_prefix: str = \"Outdoor\"\r\n) -> dict:\r\n \"\"\"\r\n Create realistic outdoor sun lighting.\r\n \r\n Args:\r\n sun_angle: Sun elevation angle in degrees (0 = horizon, 90 = overhead)\r\n sun_rotation: Sun horizontal rotation in degrees\r\n sun_energy: Sun intensity\r\n sun_color: RGB sun color\r\n sky_type: 'NISHITA' (physical sky), 'PREETHAM', 'HOSEK_WILKIE', 'COLOR'\r\n time_of_day: 'DAWN', 'MORNING', 'NOON', 'AFTERNOON', 'SUNSET', 'CUSTOM'\r\n name_prefix: Prefix for created objects\r\n \r\n Returns:\r\n Dictionary with 'sun' and 'world' keys\r\n \r\n Example:\r\n >>> lights = create_outdoor_sun_lighting(time_of_day='SUNSET')\r\n \"\"\"\r\n result = {}\r\n \r\n # Time of day presets\r\n presets = {\r\n 'DAWN': {'angle': 5, 'energy': 2.0, 'color': (1.0, 0.7, 0.5)},\r\n 'MORNING': {'angle': 25, 'energy': 4.0, 'color': (1.0, 0.95, 0.9)},\r\n 'NOON': {'angle': 70, 'energy': 6.0, 'color': (1.0, 1.0, 1.0)},\r\n 'AFTERNOON': {'angle': 45, 'energy': 5.0, 'color': (1.0, 0.98, 0.95)},\r\n 'SUNSET': {'angle': 10, 'energy': 3.0, 'color': (1.0, 0.6, 0.3)},\r\n }\r\n \r\n if time_of_day in presets:\r\n preset = presets[time_of_day]\r\n sun_angle = preset['angle']\r\n sun_energy = preset['energy']\r\n sun_color = preset['color']\r\n \r\n # === CREATE SUN ===\r\n bpy.ops.object.light_add(type='SUN', location=(0, 0, 10))\r\n sun = bpy.context.active_object\r\n sun.name = f\"{name_prefix}_Sun\"\r\n \r\n # Calculate sun direction from angles\r\n elevation_rad = math.radians(sun_angle)\r\n rotation_rad = math.radians(sun_rotation)\r\n \r\n sun.rotation_euler = (\r\n math.pi/2 - elevation_rad, # X: pitch (elevation)\r\n 0, # Y\r\n rotation_rad # Z: horizontal rotation\r\n )\r\n \r\n sun.data.energy = sun_energy\r\n sun.data.color = sun_color\r\n sun.data.angle = 0.009 # Soft shadows (angular diameter of sun)\r\n \r\n result['sun'] = sun\r\n \r\n # === CREATE SKY ===\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(f\"{name_prefix}_World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n if sky_type == 'NISHITA':\r\n # Physical sky simulation\r\n sky_texture = nodes.new('ShaderNodeTexSky')\r\n sky_texture.sky_type = 'NISHITA'\r\n sky_texture.sun_elevation = math.radians(sun_angle)\r\n sky_texture.sun_rotation = math.radians(sun_rotation)\r\n sky_texture.altitude = 0\r\n sky_texture.air_density = 1.0\r\n sky_texture.dust_density = 1.0\r\n sky_texture.ozone_density = 1.0\r\n \r\n # Adjust for time of day\r\n if time_of_day == 'SUNSET' or time_of_day == 'DAWN':\r\n sky_texture.air_density = 1.5\r\n sky_texture.dust_density = 2.0\r\n \r\n background = nodes.new('ShaderNodeBackground')\r\n background.inputs['Strength'].default_value = 1.0\r\n \r\n output = nodes.new('ShaderNodeOutputWorld')\r\n \r\n links.new(sky_texture.outputs['Color'], background.inputs['Color'])\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n \r\n elif sky_type == 'COLOR':\r\n # Simple gradient sky\r\n background = nodes.new('ShaderNodeBackground')\r\n \r\n if time_of_day == 'SUNSET':\r\n background.inputs['Color'].default_value = (0.8, 0.4, 0.3, 1.0)\r\n elif time_of_day == 'DAWN':\r\n background.inputs['Color'].default_value = (0.6, 0.3, 0.4, 1.0)\r\n else:\r\n background.inputs['Color'].default_value = (0.4, 0.6, 0.9, 1.0)\r\n \r\n background.inputs['Strength'].default_value = 0.5\r\n \r\n output = nodes.new('ShaderNodeOutputWorld')\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n \r\n result['world'] = world\r\n \r\n return result\r\n\r\n\r\ndef create_golden_hour_lighting(\r\n direction: float = 180,\r\n name_prefix: str = \"GoldenHour\"\r\n) -> dict:\r\n \"\"\"\r\n Create golden hour (sunset/sunrise) lighting.\r\n \r\n Args:\r\n direction: Sun direction (180 = behind camera)\r\n name_prefix: Prefix for objects\r\n \r\n Returns:\r\n Dictionary with lighting objects\r\n \"\"\"\r\n return create_outdoor_sun_lighting(\r\n sun_angle=15,\r\n sun_rotation=direction,\r\n sun_energy=3.5,\r\n sun_color=(1.0, 0.65, 0.35),\r\n sky_type='NISHITA',\r\n time_of_day='SUNSET',\r\n name_prefix=name_prefix\r\n )\r\n\r\n\r\ndef create_overcast_lighting(\r\n brightness: float = 3.0,\r\n name_prefix: str = \"Overcast\"\r\n) -> dict:\r\n \"\"\"\r\n Create overcast/cloudy day lighting.\r\n \r\n Args:\r\n brightness: Overall scene brightness\r\n name_prefix: Prefix for objects\r\n \r\n Returns:\r\n Dictionary with lighting objects\r\n \"\"\"\r\n result = {}\r\n \r\n # Large soft area light from above (simulating diffused sky)\r\n bpy.ops.object.light_add(type='AREA', location=(0, 0, 20))\r\n sky_light = bpy.context.active_object\r\n sky_light.name = f\"{name_prefix}_SkyLight\"\r\n sky_light.data.shape = 'DISK'\r\n sky_light.data.size = 50\r\n sky_light.data.energy = brightness * 5000\r\n sky_light.data.color = (0.9, 0.95, 1.0)\r\n sky_light.rotation_euler = (0, 0, 0)\r\n \r\n result['sky_light'] = sky_light\r\n \r\n # Soft fill from horizon\r\n bpy.ops.object.light_add(type='AREA', location=(0, -20, 5))\r\n fill = bpy.context.active_object\r\n fill.name = f\"{name_prefix}_Fill\"\r\n fill.data.shape = 'RECTANGLE'\r\n fill.data.size = 30\r\n fill.data.size_y = 10\r\n fill.data.energy = brightness * 1000\r\n fill.data.color = (0.8, 0.85, 0.9)\r\n fill.rotation_euler = (math.radians(80), 0, 0)\r\n \r\n result['fill'] = fill\r\n \r\n # Gray sky\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(f\"{name_prefix}_World\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n background = nodes.new('ShaderNodeBackground')\r\n background.inputs['Color'].default_value = (0.6, 0.65, 0.7, 1.0)\r\n background.inputs['Strength'].default_value = 0.3\r\n \r\n output = nodes.new('ShaderNodeOutputWorld')\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n \r\n result['world'] = world\r\n \r\n return result\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n # Create noon lighting\r\n lighting = create_outdoor_sun_lighting(time_of_day='NOON')\r\n print(f\"Created outdoor lighting: {list(lighting.keys())}\")","category":"tasks/lighting","source_file":"tasks\\lighting\\outdoor_sun.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Studio Three-Point Lighting Setup. Creates a professional three-point lighting setup with key, fill, and rim lights for product or character rendering.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_three_point_lighting(\r\n target_location: tuple = (0, 0, 0),\r\n key_energy: float = 1000,\r\n fill_ratio: float = 0.5,\r\n rim_energy: float = 800,\r\n key_color: tuple = (1.0, 0.98, 0.95),\r\n fill_color: tuple = (0.9, 0.95, 1.0),\r\n rim_color: tuple = (1.0, 1.0, 1.0),\r\n distance: float = 5.0,\r\n name_prefix: str = \"Studio\"\r\n) -> dict:\r\n \"\"\"\r\n Create a professional three-point lighting setup.\r\n \r\n The three-point lighting system consists of:\r\n - Key Light: Main light source, brightest, creates primary shadows\r\n - Fill Light: Softer light to fill in shadows from key light\r\n - Rim/Back Light: Creates edge highlights and separates subject from background\r\n \r\n Args:\r\n target_location: Center point the lights will illuminate\r\n key_energy: Key light intensity in watts\r\n fill_ratio: Fill light as ratio of key light (0.3-0.7 typical)\r\n rim_energy: Rim light intensity\r\n key_color: RGB color for key light (warm recommended)\r\n fill_color: RGB color for fill light (slightly cool for contrast)\r\n rim_color: RGB color for rim light\r\n distance: Distance of lights from target\r\n name_prefix: Prefix for light names\r\n \r\n Returns:\r\n Dictionary with 'key', 'fill', 'rim' light objects\r\n \r\n Example:\r\n >>> lights = create_three_point_lighting(target_location=(0, 0, 1), key_energy=1500)\r\n >>> lights['key'].data.energy = 2000 # Adjust key light\r\n \"\"\"\r\n tx, ty, tz = target_location\r\n lights = {}\r\n \r\n # === KEY LIGHT ===\r\n # Positioned high, to the front-right of subject\r\n key_x = tx + distance * 0.7\r\n key_y = ty - distance * 0.7\r\n key_z = tz + distance * 0.6\r\n \r\n bpy.ops.object.light_add(type='AREA', location=(key_x, key_y, key_z))\r\n key_light = bpy.context.active_object\r\n key_light.name = f\"{name_prefix}_KeyLight\"\r\n key_light.data.energy = key_energy\r\n key_light.data.color = key_color\r\n key_light.data.shape = 'RECTANGLE'\r\n key_light.data.size = 2.0\r\n key_light.data.size_y = 1.5\r\n \r\n # Point at target\r\n direction = (tx - key_x, ty - key_y, tz - key_z)\r\n key_light.rotation_euler = _direction_to_euler(direction)\r\n lights['key'] = key_light\r\n \r\n # === FILL LIGHT ===\r\n # Positioned to the front-left, lower and softer\r\n fill_x = tx - distance * 0.6\r\n fill_y = ty - distance * 0.5\r\n fill_z = tz + distance * 0.3\r\n \r\n bpy.ops.object.light_add(type='AREA', location=(fill_x, fill_y, fill_z))\r\n fill_light = bpy.context.active_object\r\n fill_light.name = f\"{name_prefix}_FillLight\"\r\n fill_light.data.energy = key_energy * fill_ratio\r\n fill_light.data.color = fill_color\r\n fill_light.data.shape = 'RECTANGLE'\r\n fill_light.data.size = 3.0 # Larger = softer\r\n fill_light.data.size_y = 2.5\r\n \r\n direction = (tx - fill_x, ty - fill_y, tz - fill_z)\r\n fill_light.rotation_euler = _direction_to_euler(direction)\r\n lights['fill'] = fill_light\r\n \r\n # === RIM LIGHT ===\r\n # Positioned behind and above the subject\r\n rim_x = tx - distance * 0.3\r\n rim_y = ty + distance * 0.8\r\n rim_z = tz + distance * 0.8\r\n \r\n bpy.ops.object.light_add(type='AREA', location=(rim_x, rim_y, rim_z))\r\n rim_light = bpy.context.active_object\r\n rim_light.name = f\"{name_prefix}_RimLight\"\r\n rim_light.data.energy = rim_energy\r\n rim_light.data.color = rim_color\r\n rim_light.data.shape = 'RECTANGLE'\r\n rim_light.data.size = 1.5\r\n rim_light.data.size_y = 1.0\r\n \r\n direction = (tx - rim_x, ty - rim_y, tz - rim_z)\r\n rim_light.rotation_euler = _direction_to_euler(direction)\r\n lights['rim'] = rim_light\r\n \r\n return lights\r\n\r\n\r\ndef _direction_to_euler(direction: tuple) -> tuple:\r\n \"\"\"Convert direction vector to euler rotation.\"\"\"\r\n from mathutils import Vector\r\n vec = Vector(direction).normalized()\r\n # Point -Z axis towards target (standard for lights/cameras)\r\n rot_quat = vec.to_track_quat('-Z', 'Y')\r\n return rot_quat.to_euler()\r\n\r\n\r\ndef create_product_lighting(\r\n target_location: tuple = (0, 0, 0),\r\n backdrop_color: tuple = (1.0, 1.0, 1.0),\r\n name_prefix: str = \"Product\"\r\n) -> dict:\r\n \"\"\"\r\n Create a complete product photography lighting setup with backdrop.\r\n \r\n Args:\r\n target_location: Product center location\r\n backdrop_color: RGB color for the background\r\n name_prefix: Prefix for created objects\r\n \r\n Returns:\r\n Dictionary with lights and backdrop objects\r\n \r\n Example:\r\n >>> setup = create_product_lighting((0, 0, 0.5))\r\n \"\"\"\r\n result = {}\r\n tx, ty, tz = target_location\r\n \r\n # Create three-point lighting\r\n result['lights'] = create_three_point_lighting(\r\n target_location=target_location,\r\n key_energy=800,\r\n fill_ratio=0.6,\r\n rim_energy=500,\r\n distance=4.0,\r\n name_prefix=name_prefix\r\n )\r\n \r\n # Create backdrop (curved plane)\r\n bpy.ops.mesh.primitive_plane_add(size=10, location=(tx, ty + 5, tz - 0.5))\r\n backdrop = bpy.context.active_object\r\n backdrop.name = f\"{name_prefix}_Backdrop\"\r\n backdrop.rotation_euler[0] = math.radians(90)\r\n \r\n # Add bend modifier for curved backdrop\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n backdrop.modifiers[\"SimpleDeform\"].deform_method = 'BEND'\r\n backdrop.modifiers[\"SimpleDeform\"].angle = math.radians(-45)\r\n backdrop.modifiers[\"SimpleDeform\"].deform_axis = 'X'\r\n \r\n # Backdrop material\r\n backdrop_mat = bpy.data.materials.new(name=f\"{name_prefix}_BackdropMat\")\r\n bsdf = backdrop_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*backdrop_color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n backdrop.data.materials.append(backdrop_mat)\r\n result['backdrop'] = backdrop\r\n \r\n # Set world to white for clean reflections\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n bg = world.node_tree.nodes.get(\"Background\")\r\n if bg:\r\n bg.inputs['Color'].default_value = (*backdrop_color, 1.0)\r\n bg.inputs['Strength'].default_value = 0.5\r\n \r\n return result\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n # Create a three-point lighting setup\r\n lights = create_three_point_lighting(\r\n target_location=(0, 0, 1),\r\n key_energy=1200,\r\n fill_ratio=0.4,\r\n rim_energy=800\r\n )\r\n print(f\"Created {len(lights)} lights: {list(lights.keys())}\")","category":"tasks/lighting","source_file":"tasks\\lighting\\studio_three_point.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Axe Generator. Generates battle axes and hand axes.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_axe(\r\n handle_length: float = 0.6,\r\n head_size: float = 0.15,\r\n style: str = 'BATTLE',\r\n material_type: str = 'IRON',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Axe\"\r\n) -> dict:\r\n \"\"\"\r\n Create an axe.\r\n \r\n Args:\r\n handle_length: Handle length\r\n head_size: Axe head size\r\n style: 'BATTLE', 'HAND', 'DOUBLE'\r\n material_type: 'IRON', 'BRONZE', 'STEEL'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with axe parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Handle\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.02,\r\n depth=handle_length,\r\n location=(location[0], location[1], location[2] + handle_length/2)\r\n )\r\n handle = bpy.context.active_object\r\n handle.name = f\"{name}_Handle\"\r\n \r\n handle_mat = bpy.data.materials.new(f\"{name}_HandleMat\")\r\n bsdf = handle_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.2, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n handle.data.materials.append(handle_mat)\r\n \r\n result['handle'] = handle\r\n \r\n # Axe head\r\n head_z = location[2] + handle_length - head_size * 0.3\r\n \r\n if style == 'DOUBLE':\r\n heads = [_create_axe_blade(head_size, (location[0], location[1], head_z), name, 1),\r\n _create_axe_blade(head_size, (location[0], location[1], head_z), name, -1)]\r\n result['heads'] = heads\r\n else:\r\n head = _create_axe_blade(head_size, (location[0], location[1], head_z), name, 1)\r\n \r\n if style == 'HAND':\r\n head.scale *= 0.6\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n result['head'] = head\r\n \r\n # Metal material\r\n metal_colors = {\r\n 'IRON': (0.4, 0.4, 0.42),\r\n 'BRONZE': (0.8, 0.5, 0.2),\r\n 'STEEL': (0.6, 0.6, 0.65)\r\n }\r\n \r\n metal_mat = bpy.data.materials.new(f\"{name}_MetalMat\")\r\n bsdf = metal_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*metal_colors.get(material_type, metal_colors['IRON']), 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.35\r\n \r\n if 'head' in result:\r\n result['head'].data.materials.append(metal_mat)\r\n if 'heads' in result:\r\n for h in result['heads']:\r\n h.data.materials.append(metal_mat)\r\n \r\n return result\r\n\r\n\r\ndef _create_axe_blade(\r\n size: float,\r\n location: tuple,\r\n name: str,\r\n direction: int = 1\r\n) -> bpy.types.Object:\r\n \"\"\"Create axe blade.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + direction * size * 0.5,\r\n location[1],\r\n location[2]\r\n ))\r\n blade = bpy.context.active_object\r\n blade.name = f\"{name}_Blade\"\r\n blade.scale = (size, 0.02, size * 0.8)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Shape blade\r\n for v in blade.data.vertices:\r\n if direction * v.co.x > 0:\r\n # Curve cutting edge\r\n v.co.z *= 1 - abs(v.co.x) * 0.5\r\n v.co.x *= 0.7\r\n \r\n bpy.ops.object.shade_flat()\r\n \r\n return blade\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_axe(style='BATTLE', location=(0, 0, 0))\r\n create_axe(style='HAND', location=(0.4, 0, 0))\r\n create_axe(style='DOUBLE', location=(0.8, 0, 0))\r\n \r\n print(\"Created axes\")","category":"tasks/modeling","source_file":"tasks\\modeling\\axe_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Banner Generator. Generates banners and flags with cloth simulation ready.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_banner(\r\n width: float = 0.5,\r\n height: float = 1.0,\r\n style: str = 'HANGING',\r\n color: tuple = (0.7, 0.1, 0.1),\r\n with_pole: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Banner\"\r\n) -> dict:\r\n \"\"\"\r\n Create a banner/flag.\r\n \r\n Args:\r\n width: Banner width\r\n height: Banner height\r\n style: 'HANGING', 'FLAG', 'PENNANT'\r\n color: RGB banner color\r\n with_pole: Add pole/staff\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with banner parts\r\n \"\"\"\r\n result = {}\r\n \r\n # === CLOTH ===\r\n if style == 'PENNANT':\r\n bpy.ops.mesh.primitive_cone_add(\r\n vertices=3,\r\n radius1=width/2,\r\n radius2=0,\r\n depth=0.01,\r\n location=(location[0], location[1], location[2] - height/2)\r\n )\r\n cloth = bpy.context.active_object\r\n cloth.rotation_euler.x = math.radians(90)\r\n cloth.rotation_euler.z = math.radians(90)\r\n else:\r\n bpy.ops.mesh.primitive_plane_add(\r\n size=1,\r\n location=(location[0], location[1], location[2] - height/2)\r\n )\r\n cloth = bpy.context.active_object\r\n cloth.scale = (width/2, height/2, 1)\r\n cloth.rotation_euler.x = math.radians(90)\r\n \r\n bpy.ops.object.transform_apply(scale=True, rotation=True)\r\n cloth.name = f\"{name}_Cloth\"\r\n \r\n # Subdivide for cloth sim\r\n bpy.ops.object.modifier_add(type='SUBSURF')\r\n cloth.modifiers[\"Subdivision\"].levels = 3\r\n \r\n # Cloth material\r\n cloth_mat = bpy.data.materials.new(f\"{name}_ClothMat\")\r\n bsdf = cloth_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n bsdf.inputs['Sheen Weight'].default_value = 0.3\r\n cloth.data.materials.append(cloth_mat)\r\n \r\n result['cloth'] = cloth\r\n \r\n # === POLE ===\r\n if with_pole:\r\n pole_height = height + 0.5 if style == 'FLAG' else height + 0.2\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.02,\r\n depth=pole_height,\r\n location=(\r\n location[0] - width/2 - 0.02 if style != 'HANGING' else location[0],\r\n location[1],\r\n location[2] if style == 'HANGING' else location[2] + 0.2\r\n )\r\n )\r\n pole = bpy.context.active_object\r\n pole.name = f\"{name}_Pole\"\r\n \r\n if style == 'FLAG':\r\n pole.rotation_euler.x = math.radians(15)\r\n \r\n pole_mat = bpy.data.materials.new(f\"{name}_PoleMat\")\r\n bsdf = pole_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.2, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n pole.data.materials.append(pole_mat)\r\n \r\n result['pole'] = pole\r\n \r\n return result\r\n\r\n\r\ndef add_cloth_simulation(\r\n cloth_obj: bpy.types.Object,\r\n pin_group: str = None,\r\n stiffness: float = 15.0\r\n) -> bpy.types.Modifier:\r\n \"\"\"\r\n Add cloth simulation to banner.\r\n \r\n Args:\r\n cloth_obj: The banner cloth object\r\n pin_group: Vertex group name to pin\r\n stiffness: Cloth stiffness\r\n \r\n Returns:\r\n The cloth modifier\r\n \"\"\"\r\n mod = cloth_obj.modifiers.new(\"Cloth\", 'CLOTH')\r\n settings = mod.settings\r\n \r\n settings.quality = 5\r\n settings.mass = 0.3\r\n settings.tension_stiffness = stiffness\r\n settings.compression_stiffness = stiffness\r\n settings.bending_stiffness = 5.0\r\n \r\n if pin_group:\r\n settings.vertex_group_mass = pin_group\r\n \r\n return mod\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_banner(style='HANGING', location=(0, 0, 2))\r\n create_banner(style='FLAG', color=(0.1, 0.2, 0.7), location=(1, 0, 0))\r\n create_banner(style='PENNANT', location=(2, 0, 2))\r\n \r\n print(\"Created banners\")","category":"tasks/modeling","source_file":"tasks\\modeling\\banner_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Barrel Generator. Generates wooden and metal barrels.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_barrel(\r\n height: float = 0.8,\r\n radius: float = 0.25,\r\n bulge: float = 0.1,\r\n staves: int = 12,\r\n bands: int = 3,\r\n style: str = 'WOODEN',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Barrel\"\r\n) -> dict:\r\n \"\"\"\r\n Create a barrel.\r\n \r\n Args:\r\n height: Barrel height\r\n radius: Base radius\r\n bulge: Middle bulge amount\r\n staves: Number of wooden staves\r\n bands: Number of metal bands\r\n style: 'WOODEN', 'METAL'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with barrel parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Main body\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n vertices=staves,\r\n radius=radius,\r\n depth=height,\r\n location=(location[0], location[1], location[2] + height/2)\r\n )\r\n barrel = bpy.context.active_object\r\n barrel.name = name\r\n \r\n # Add bulge in middle\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n for v in barrel.data.vertices:\r\n # Distance from middle determines bulge\r\n z_normalized = abs(v.co.z) / (height/2)\r\n bulge_factor = 1 + bulge * (1 - z_normalized**2)\r\n \r\n if v.co.x != 0 or v.co.y != 0:\r\n angle = math.atan2(v.co.y, v.co.x)\r\n dist = math.sqrt(v.co.x**2 + v.co.y**2) * bulge_factor\r\n v.co.x = math.cos(angle) * dist\r\n v.co.y = math.sin(angle) * dist\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n # Material\r\n if style == 'WOODEN':\r\n mat = bpy.data.materials.new(f\"{name}_WoodMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.28, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n else: # METAL\r\n mat = bpy.data.materials.new(f\"{name}_MetalMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.5, 0.5, 0.55, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n \r\n barrel.data.materials.append(mat)\r\n result['barrel'] = barrel\r\n \r\n # Metal bands\r\n if style == 'WOODEN':\r\n band_objs = _create_barrel_bands(\r\n radius + bulge, height, bands, location, name\r\n )\r\n result['bands'] = band_objs\r\n \r\n return result\r\n\r\n\r\ndef _create_barrel_bands(\r\n radius: float,\r\n height: float,\r\n count: int,\r\n location: tuple,\r\n name: str\r\n) -> list:\r\n \"\"\"Create metal bands around barrel.\"\"\"\r\n bands = []\r\n band_height = 0.03\r\n \r\n band_mat = bpy.data.materials.new(f\"{name}_BandMat\")\r\n bsdf = band_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.3, 0.32, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.35\r\n \r\n positions = []\r\n if count == 1:\r\n positions = [0.5]\r\n elif count == 2:\r\n positions = [0.2, 0.8]\r\n elif count >= 3:\r\n positions = [0.15, 0.5, 0.85]\r\n for i in range(3, count):\r\n positions.append(i / (count + 1))\r\n \r\n for i, pos in enumerate(positions):\r\n z = location[2] + height * pos\r\n \r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=radius * 1.02,\r\n minor_radius=band_height/2,\r\n location=(location[0], location[1], z)\r\n )\r\n band = bpy.context.active_object\r\n band.name = f\"{name}_Band_{i+1}\"\r\n band.data.materials.append(band_mat)\r\n bands.append(band)\r\n \r\n return bands\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_barrel(style='WOODEN', location=(0, 0, 0))\r\n create_barrel(style='METAL', location=(0.8, 0, 0))\r\n \r\n print(\"Created barrels\")","category":"tasks/modeling","source_file":"tasks\\modeling\\barrel_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Book Generator. Generates books and book stacks with customizable covers.","output":"import bpy\r\nimport random\r\n\r\n\r\ndef create_book(\r\n width: float = 0.15,\r\n height: float = 0.22,\r\n depth: float = 0.03,\r\n cover_color: tuple = None,\r\n with_pages: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Book\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a simple book.\r\n \r\n Args:\r\n width: Book width (spine to edge)\r\n height: Book height\r\n depth: Book thickness\r\n cover_color: RGB cover color (random if None)\r\n with_pages: Add visible page edges\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The book object\r\n \"\"\"\r\n if cover_color is None:\r\n cover_color = (\r\n random.uniform(0.2, 0.8),\r\n random.uniform(0.2, 0.8),\r\n random.uniform(0.2, 0.8)\r\n )\r\n \r\n # Main book body\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + width/2,\r\n location[1],\r\n location[2] + height/2\r\n ))\r\n book = bpy.context.active_object\r\n book.name = name\r\n book.scale = (width/2, depth/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Cover material\r\n cover_mat = bpy.data.materials.new(f\"{name}_CoverMat\")\r\n bsdf = cover_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*cover_color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n book.data.materials.append(cover_mat)\r\n \r\n # Page edges\r\n if with_pages:\r\n page_inset = 0.005\r\n page_height = height - 0.01\r\n \r\n # Pages material (cream/white)\r\n page_mat = bpy.data.materials.new(f\"{name}_PageMat\")\r\n bsdf = page_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.93, 0.88, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n \r\n # Top edge\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + width/2,\r\n location[1],\r\n location[2] + height - page_inset\r\n ))\r\n pages_top = bpy.context.active_object\r\n pages_top.scale = ((width - 0.01)/2, (depth - 0.01)/2, page_inset/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n pages_top.data.materials.append(page_mat)\r\n pages_top.parent = book\r\n \r\n # Front edge\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + width - page_inset,\r\n location[1],\r\n location[2] + height/2\r\n ))\r\n pages_front = bpy.context.active_object\r\n pages_front.scale = (page_inset/2, (depth - 0.01)/2, page_height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n pages_front.data.materials.append(page_mat)\r\n pages_front.parent = book\r\n \r\n return book\r\n\r\n\r\ndef create_book_stack(\r\n count: int = 5,\r\n min_height: float = 0.18,\r\n max_height: float = 0.28,\r\n random_rotation: bool = True,\r\n location: tuple = (0, 0, 0),\r\n seed: int = 42,\r\n name: str = \"BookStack\"\r\n) -> list:\r\n \"\"\"\r\n Create a stack of books.\r\n \r\n Args:\r\n count: Number of books\r\n min_height: Minimum book height\r\n max_height: Maximum book height\r\n random_rotation: Slightly rotate books\r\n location: Stack position\r\n seed: Random seed\r\n name: Stack name\r\n \r\n Returns:\r\n List of book objects\r\n \"\"\"\r\n random.seed(seed)\r\n books = []\r\n current_z = location[2]\r\n \r\n for i in range(count):\r\n height = random.uniform(min_height, max_height)\r\n width = random.uniform(0.12, 0.18)\r\n depth = random.uniform(0.02, 0.05)\r\n \r\n book = create_book(\r\n width=width,\r\n height=height,\r\n depth=depth,\r\n location=(location[0], location[1], current_z),\r\n name=f\"{name}_{i+1}\"\r\n )\r\n \r\n # Rotate book to lay flat\r\n book.rotation_euler.x = 1.5708\r\n \r\n if random_rotation:\r\n book.rotation_euler.z = random.uniform(-0.15, 0.15)\r\n book.location.x += random.uniform(-0.02, 0.02)\r\n book.location.y += random.uniform(-0.02, 0.02)\r\n \r\n current_z += depth\r\n books.append(book)\r\n \r\n return books\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_book(location=(0, 0, 0))\r\n create_book_stack(count=6, location=(0.5, 0, 0))\r\n \r\n print(\"Created book and book stack\")","category":"tasks/modeling","source_file":"tasks\\modeling\\book_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Bottle Generator. Generates various bottle types with glass materials.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_bottle(\r\n height: float = 0.3,\r\n body_radius: float = 0.04,\r\n neck_radius: float = 0.015,\r\n neck_height: float = 0.06,\r\n style: str = 'WINE',\r\n glass_color: tuple = (0.1, 0.5, 0.2),\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Bottle\"\r\n) -> dict:\r\n \"\"\"\r\n Create a bottle.\r\n \r\n Args:\r\n height: Total bottle height\r\n body_radius: Body radius\r\n neck_radius: Neck radius\r\n neck_height: Neck length\r\n style: 'WINE', 'BEER', 'SODA', 'PERFUME'\r\n glass_color: RGB glass tint\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with bottle parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Style adjustments\r\n styles = {\r\n 'WINE': {'body_ratio': 0.65, 'shoulder': 0.2, 'neck_taper': True},\r\n 'BEER': {'body_ratio': 0.75, 'shoulder': 0.1, 'neck_taper': False},\r\n 'SODA': {'body_ratio': 0.7, 'shoulder': 0.15, 'neck_taper': False},\r\n 'PERFUME': {'body_ratio': 0.5, 'shoulder': 0.3, 'neck_taper': True}\r\n }\r\n \r\n s = styles.get(style, styles['WINE'])\r\n body_height = height * s['body_ratio']\r\n shoulder_height = height * s['shoulder']\r\n \r\n # Body cylinder\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=body_radius,\r\n depth=body_height,\r\n location=(location[0], location[1], location[2] + body_height/2)\r\n )\r\n body = bpy.context.active_object\r\n body.name = f\"{name}_Body\"\r\n \r\n # Shoulder (cone)\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=body_radius,\r\n radius2=neck_radius,\r\n depth=shoulder_height,\r\n location=(\r\n location[0], location[1],\r\n location[2] + body_height + shoulder_height/2\r\n )\r\n )\r\n shoulder = bpy.context.active_object\r\n shoulder.name = f\"{name}_Shoulder\"\r\n \r\n # Neck\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=neck_radius,\r\n depth=neck_height,\r\n location=(\r\n location[0], location[1],\r\n location[2] + body_height + shoulder_height + neck_height/2\r\n )\r\n )\r\n neck = bpy.context.active_object\r\n neck.name = f\"{name}_Neck\"\r\n \r\n # Join parts\r\n bpy.ops.object.select_all(action='DESELECT')\r\n body.select_set(True)\r\n shoulder.select_set(True)\r\n neck.select_set(True)\r\n bpy.context.view_layer.objects.active = body\r\n bpy.ops.object.join()\r\n \r\n bottle = bpy.context.active_object\r\n bottle.name = name\r\n bpy.ops.object.shade_smooth()\r\n \r\n result['bottle'] = bottle\r\n \r\n # Glass material\r\n glass_mat = bpy.data.materials.new(f\"{name}_GlassMat\")\r\n glass_mat.blend_method = 'BLEND'\r\n bsdf = glass_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*glass_color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.05\r\n bsdf.inputs['IOR'].default_value = 1.52\r\n bsdf.inputs['Transmission Weight'].default_value = 0.95\r\n bottle.data.materials.append(glass_mat)\r\n \r\n result['material'] = glass_mat\r\n \r\n # Cap/Cork\r\n cap = _create_bottle_cap(\r\n location[0], location[1],\r\n location[2] + height + 0.005,\r\n neck_radius, style, name\r\n )\r\n result['cap'] = cap\r\n \r\n return result\r\n\r\n\r\ndef _create_bottle_cap(\r\n x: float, y: float, z: float,\r\n radius: float, style: str, name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create bottle cap or cork.\"\"\"\r\n if style == 'WINE':\r\n # Cork\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius * 0.9,\r\n depth=0.025,\r\n location=(x, y, z)\r\n )\r\n cap = bpy.context.active_object\r\n cap.name = f\"{name}_Cork\"\r\n \r\n mat = bpy.data.materials.new(f\"{name}_CorkMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.5, 0.35, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n cap.data.materials.append(mat)\r\n else:\r\n # Metal cap\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius * 1.2,\r\n depth=0.015,\r\n location=(x, y, z)\r\n )\r\n cap = bpy.context.active_object\r\n cap.name = f\"{name}_Cap\"\r\n \r\n mat = bpy.data.materials.new(f\"{name}_CapMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.7, 0.7, 0.75, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.3\r\n cap.data.materials.append(mat)\r\n \r\n return cap\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_bottle(style='WINE', location=(0, 0, 0))\r\n create_bottle(style='BEER', glass_color=(0.3, 0.25, 0.1), location=(0.15, 0, 0))\r\n create_bottle(style='SODA', glass_color=(0.1, 0.1, 0.1), location=(0.3, 0, 0))\r\n \r\n print(\"Created 3 bottle types\")","category":"tasks/modeling","source_file":"tasks\\modeling\\bottle_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Bow and Arrow Generator. Generates bows and arrows for game/fantasy props.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_bow(\r\n size: float = 0.8,\r\n draw_amount: float = 0,\r\n style: str = 'RECURVE',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Bow\"\r\n) -> dict:\r\n \"\"\"\r\n Create a bow.\r\n \r\n Args:\r\n size: Bow size\r\n draw_amount: How much string is drawn (0-1)\r\n style: 'RECURVE', 'LONGBOW', 'SHORTBOW'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with bow parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Bow curve\r\n curve_data = bpy.data.curves.new(f\"{name}_Curve\", 'CURVE')\r\n curve_data.dimensions = '3D'\r\n \r\n spline = curve_data.splines.new('BEZIER')\r\n spline.bezier_points.add(2) # 3 total points\r\n \r\n points = spline.bezier_points\r\n \r\n # Top\r\n points[0].co = (0, 0, size/2)\r\n points[0].handle_left = (-0.1, 0, size/2)\r\n points[0].handle_right = (0.1, 0, size/2)\r\n \r\n # Middle (grip)\r\n points[1].co = (-0.05 - draw_amount * 0.15, 0, 0)\r\n points[1].handle_left = (-0.05 - draw_amount * 0.15, 0, size * 0.2)\r\n points[1].handle_right = (-0.05 - draw_amount * 0.15, 0, -size * 0.2)\r\n \r\n # Bottom\r\n points[2].co = (0, 0, -size/2)\r\n points[2].handle_left = (0.1, 0, -size/2)\r\n points[2].handle_right = (-0.1, 0, -size/2)\r\n \r\n # Style adjustments\r\n if style == 'RECURVE':\r\n points[0].co.x = 0.08\r\n points[2].co.x = 0.08\r\n elif style == 'LONGBOW':\r\n size *= 1.3\r\n points[0].co.z = size/2\r\n points[2].co.z = -size/2\r\n \r\n # Bevel for thickness\r\n curve_data.bevel_depth = 0.015\r\n curve_data.bevel_resolution = 4\r\n \r\n bow = bpy.data.objects.new(name, curve_data)\r\n bpy.context.collection.objects.link(bow)\r\n bow.location = location\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.28, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n bow.data.materials.append(mat)\r\n \r\n result['bow'] = bow\r\n \r\n # Bowstring\r\n string = _create_bowstring(size, draw_amount, location, name)\r\n result['string'] = string\r\n \r\n return result\r\n\r\n\r\ndef _create_bowstring(\r\n size: float,\r\n draw: float,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create bowstring.\"\"\"\r\n curve_data = bpy.data.curves.new(f\"{name}_StringCurve\", 'CURVE')\r\n curve_data.dimensions = '3D'\r\n \r\n spline = curve_data.splines.new('POLY')\r\n spline.points.add(2) # 3 points\r\n \r\n spline.points[0].co = (0, 0, size/2, 1)\r\n spline.points[1].co = (-0.1 - draw * 0.2, 0, 0, 1)\r\n spline.points[2].co = (0, 0, -size/2, 1)\r\n \r\n curve_data.bevel_depth = 0.002\r\n \r\n string = bpy.data.objects.new(f\"{name}_String\", curve_data)\r\n bpy.context.collection.objects.link(string)\r\n string.location = location\r\n \r\n mat = bpy.data.materials.new(f\"{name}_StringMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.75, 0.6, 1.0)\r\n string.data.materials.append(mat)\r\n \r\n return string\r\n\r\n\r\ndef create_arrow(\r\n length: float = 0.6,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Arrow\"\r\n) -> dict:\r\n \"\"\"Create an arrow.\"\"\"\r\n result = {}\r\n \r\n # Shaft\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.005,\r\n depth=length,\r\n location=(location[0] + length/2, location[1], location[2])\r\n )\r\n shaft = bpy.context.active_object\r\n shaft.name = f\"{name}_Shaft\"\r\n shaft.rotation_euler.y = math.radians(90)\r\n bpy.ops.object.transform_apply(rotation=True)\r\n \r\n shaft_mat = bpy.data.materials.new(f\"{name}_ShaftMat\")\r\n bsdf = shaft_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.5, 0.4, 0.25, 1.0)\r\n shaft.data.materials.append(shaft_mat)\r\n \r\n result['shaft'] = shaft\r\n \r\n # Arrowhead\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=0.01,\r\n radius2=0,\r\n depth=0.04,\r\n location=(location[0] + length + 0.02, location[1], location[2])\r\n )\r\n head = bpy.context.active_object\r\n head.name = f\"{name}_Head\"\r\n head.rotation_euler.y = math.radians(90)\r\n \r\n head_mat = bpy.data.materials.new(f\"{name}_HeadMat\")\r\n bsdf = head_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.5, 0.5, 0.55, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n head.data.materials.append(head_mat)\r\n \r\n result['head'] = head\r\n \r\n # Fletching\r\n for i in range(3):\r\n angle = i * 120\r\n bpy.ops.mesh.primitive_plane_add(\r\n size=0.04,\r\n location=(location[0], location[1], location[2])\r\n )\r\n fletch = bpy.context.active_object\r\n fletch.name = f\"{name}_Fletch_{i}\"\r\n fletch.scale = (0.5, 1, 1)\r\n fletch.rotation_euler = (math.radians(angle), math.radians(90), 0)\r\n \r\n result['fletching'] = True\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_bow(draw_amount=0, location=(0, 0, 0))\r\n create_bow(draw_amount=0.8, location=(0.3, 0, 0))\r\n create_arrow(location=(0, 0.3, 0))\r\n \r\n print(\"Created bow and arrow\")","category":"tasks/modeling","source_file":"tasks\\modeling\\bow_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Candle Generator. Generates candles with optional flames and holders.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_candle(\r\n height: float = 0.15,\r\n radius: float = 0.015,\r\n lit: bool = True,\r\n melted: bool = False,\r\n color: tuple = (0.95, 0.9, 0.85),\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Candle\"\r\n) -> dict:\r\n \"\"\"\r\n Create a candle.\r\n \r\n Args:\r\n height: Candle height\r\n radius: Candle radius\r\n lit: Add flame\r\n melted: Add melted wax drips\r\n color: RGB wax color\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with candle parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Candle body\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius,\r\n depth=height,\r\n location=(location[0], location[1], location[2] + height/2)\r\n )\r\n candle = bpy.context.active_object\r\n candle.name = name\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Wax material\r\n wax_mat = bpy.data.materials.new(f\"{name}_WaxMat\")\r\n bsdf = wax_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n bsdf.inputs['Subsurface Weight'].default_value = 0.3\r\n bsdf.inputs['Subsurface Radius'].default_value = (0.1, 0.05, 0.02)\r\n candle.data.materials.append(wax_mat)\r\n \r\n result['candle'] = candle\r\n \r\n # Wick\r\n wick_height = 0.01\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.001,\r\n depth=wick_height,\r\n location=(location[0], location[1], location[2] + height + wick_height/2)\r\n )\r\n wick = bpy.context.active_object\r\n wick.name = f\"{name}_Wick\"\r\n \r\n wick_mat = bpy.data.materials.new(f\"{name}_WickMat\")\r\n bsdf = wick_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.1, 0.1, 0.1, 1.0)\r\n wick.data.materials.append(wick_mat)\r\n \r\n result['wick'] = wick\r\n \r\n # Flame\r\n if lit:\r\n flame = _create_candle_flame(\r\n (location[0], location[1], location[2] + height + wick_height),\r\n name\r\n )\r\n result['flame'] = flame\r\n \r\n # Light\r\n bpy.ops.object.light_add(\r\n type='POINT',\r\n location=(location[0], location[1], location[2] + height + 0.02)\r\n )\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 10\r\n light.data.color = (1.0, 0.7, 0.3)\r\n light.data.shadow_soft_size = 0.05\r\n result['light'] = light\r\n \r\n # Melted wax drips\r\n if melted:\r\n drips = _create_wax_drips(radius, height, location, wax_mat, name)\r\n result['drips'] = drips\r\n \r\n return result\r\n\r\n\r\ndef _create_candle_flame(location: tuple, name: str) -> bpy.types.Object:\r\n \"\"\"Create candle flame.\"\"\"\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=0.008,\r\n location=(location[0], location[1], location[2] + 0.012)\r\n )\r\n flame = bpy.context.active_object\r\n flame.name = f\"{name}_Flame\"\r\n flame.scale.z = 2.0\r\n bpy.ops.object.transform_apply(scale=True)\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Flame material\r\n mat = bpy.data.materials.new(f\"{name}_FlameMat\")\r\n mat.blend_method = 'BLEND'\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (1.0, 0.6, 0.1, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (1.0, 0.5, 0.1, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 20.0\r\n bsdf.inputs['Alpha'].default_value = 0.9\r\n flame.data.materials.append(mat)\r\n \r\n return flame\r\n\r\n\r\ndef _create_wax_drips(\r\n radius: float,\r\n height: float,\r\n location: tuple,\r\n material: bpy.types.Material,\r\n name: str\r\n) -> list:\r\n \"\"\"Create melted wax drips.\"\"\"\r\n drips = []\r\n drip_count = random.randint(3, 6)\r\n \r\n for i in range(drip_count):\r\n angle = random.uniform(0, 2 * math.pi)\r\n start_z = random.uniform(height * 0.5, height * 0.9)\r\n drip_length = random.uniform(height * 0.2, height * 0.5)\r\n \r\n # Drip sphere chain\r\n for j in range(3):\r\n pos = (\r\n location[0] + math.cos(angle) * (radius + 0.003),\r\n location[1] + math.sin(angle) * (radius + 0.003),\r\n location[2] + start_z - j * drip_length/3\r\n )\r\n \r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=0.003 * (1 - j * 0.2),\r\n location=pos\r\n )\r\n drip = bpy.context.active_object\r\n drip.name = f\"{name}_Drip_{i}_{j}\"\r\n drip.data.materials.append(material)\r\n drips.append(drip)\r\n \r\n return drips\r\n\r\n\r\ndef create_candle_holder(\r\n candle_radius: float = 0.015,\r\n style: str = 'SIMPLE',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"CandleHolder\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a candle holder.\r\n \r\n Args:\r\n candle_radius: Radius of candle to hold\r\n style: 'SIMPLE', 'TAPER', 'DISH'\r\n location: Position\r\n name: Object name\r\n \"\"\"\r\n if style == 'SIMPLE':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=candle_radius * 2,\r\n depth=0.02,\r\n location=location\r\n )\r\n elif style == 'TAPER':\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=candle_radius * 3,\r\n radius2=candle_radius * 1.5,\r\n depth=0.03,\r\n location=(location[0], location[1], location[2] + 0.015)\r\n )\r\n else: # DISH\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=candle_radius * 4,\r\n depth=0.01,\r\n location=location\r\n )\r\n \r\n holder = bpy.context.active_object\r\n holder.name = name\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.5, 0.35, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n holder.data.materials.append(mat)\r\n \r\n return holder\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_candle(lit=True, location=(0, 0, 0))\r\n create_candle(lit=True, melted=True, location=(0.1, 0, 0))\r\n \r\n print(\"Created candles\")","category":"tasks/modeling","source_file":"tasks\\modeling\\candle_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Chair Generator. Generates various chair types with customizable dimensions.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_chair(\r\n chair_type: str = 'DINING',\r\n seat_width: float = 0.45,\r\n seat_depth: float = 0.42,\r\n seat_height: float = 0.45,\r\n back_height: float = 0.45,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Chair\"\r\n) -> dict:\r\n \"\"\"\r\n Create a procedural chair.\r\n \r\n Args:\r\n chair_type: 'DINING', 'OFFICE', 'STOOL', 'ARMCHAIR'\r\n seat_width: Seat width\r\n seat_depth: Seat depth\r\n seat_height: Height from floor to seat\r\n back_height: Backrest height (0 for stool)\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with chair parts\r\n \"\"\"\r\n result = {}\r\n \r\n if chair_type == 'STOOL':\r\n back_height = 0\r\n elif chair_type == 'ARMCHAIR':\r\n seat_width = 0.6\r\n seat_depth = 0.55\r\n \r\n seat_thickness = 0.05\r\n leg_size = 0.04\r\n \r\n # === SEAT ===\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0], location[1], location[2] + seat_height\r\n ))\r\n seat = bpy.context.active_object\r\n seat.name = f\"{name}_Seat\"\r\n seat.scale = (seat_width/2, seat_depth/2, seat_thickness/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n result['seat'] = seat\r\n \r\n # === LEGS ===\r\n leg_inset = 0.05\r\n legs = []\r\n \r\n leg_positions = [\r\n (-seat_width/2 + leg_inset, -seat_depth/2 + leg_inset),\r\n (seat_width/2 - leg_inset, -seat_depth/2 + leg_inset),\r\n (-seat_width/2 + leg_inset, seat_depth/2 - leg_inset),\r\n (seat_width/2 - leg_inset, seat_depth/2 - leg_inset)\r\n ]\r\n \r\n for i, (lx, ly) in enumerate(leg_positions):\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=leg_size,\r\n depth=seat_height,\r\n location=(location[0] + lx, location[1] + ly, location[2] + seat_height/2)\r\n )\r\n leg = bpy.context.active_object\r\n leg.name = f\"{name}_Leg_{i+1}\"\r\n legs.append(leg)\r\n \r\n result['legs'] = legs\r\n \r\n # === BACKREST ===\r\n if back_height > 0:\r\n back_thickness = 0.03\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1] + seat_depth/2 - back_thickness/2,\r\n location[2] + seat_height + back_height/2\r\n ))\r\n backrest = bpy.context.active_object\r\n backrest.name = f\"{name}_Back\"\r\n backrest.scale = (seat_width/2 - 0.02, back_thickness/2, back_height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n result['backrest'] = backrest\r\n \r\n # Back supports\r\n for side in [-1, 1]:\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=leg_size * 0.8,\r\n depth=back_height,\r\n location=(\r\n location[0] + side * (seat_width/2 - leg_inset),\r\n location[1] + seat_depth/2 - leg_inset,\r\n location[2] + seat_height + back_height/2\r\n )\r\n )\r\n support = bpy.context.active_object\r\n support.name = f\"{name}_BackSupport_{side}\"\r\n \r\n # === ARMRESTS ===\r\n if chair_type == 'ARMCHAIR':\r\n arm_height = 0.25\r\n arm_width = 0.06\r\n \r\n for side, offset in [('L', -1), ('R', 1)]:\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + offset * (seat_width/2 + arm_width/2),\r\n location[1],\r\n location[2] + seat_height + arm_height/2\r\n ))\r\n armrest = bpy.context.active_object\r\n armrest.name = f\"{name}_Armrest_{side}\"\r\n armrest.scale = (arm_width/2, seat_depth/2, arm_height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # === MATERIAL ===\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.18, 0.08, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.45\r\n \r\n for obj in result.values():\r\n if isinstance(obj, bpy.types.Object):\r\n obj.data.materials.append(mat)\r\n elif isinstance(obj, list):\r\n for o in obj:\r\n o.data.materials.append(mat)\r\n \r\n result['material'] = mat\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_chair('DINING', location=(0, 0, 0))\r\n create_chair('STOOL', location=(1.5, 0, 0))\r\n create_chair('ARMCHAIR', location=(-1.5, 0, 0))\r\n \r\n print(\"Created 3 chair variations\")","category":"tasks/modeling","source_file":"tasks\\modeling\\chair_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Treasure Chest Generator. Generates treasure chests with lids and decorations.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_chest(\r\n size: tuple = (0.5, 0.35, 0.35),\r\n lid_open: float = 0,\r\n style: str = 'WOODEN',\r\n with_treasure: bool = False,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Chest\"\r\n) -> dict:\r\n \"\"\"\r\n Create a treasure chest.\r\n \r\n Args:\r\n size: XYZ dimensions\r\n lid_open: Lid angle 0-90 degrees\r\n style: 'WOODEN', 'METAL', 'ORNATE'\r\n with_treasure: Add gold inside\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with chest parts\r\n \"\"\"\r\n result = {}\r\n \r\n w, d, h = size\r\n body_height = h * 0.6\r\n lid_height = h * 0.4\r\n \r\n # === BODY ===\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] + body_height/2\r\n ))\r\n body = bpy.context.active_object\r\n body.name = f\"{name}_Body\"\r\n body.scale = (w/2, d/2, body_height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n body_mat = _create_chest_material(style, name, \"Body\")\r\n body.data.materials.append(body_mat)\r\n result['body'] = body\r\n \r\n # === LID ===\r\n # Create curved lid\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=d/2,\r\n depth=w,\r\n location=(location[0], location[1], location[2] + body_height)\r\n )\r\n lid = bpy.context.active_object\r\n lid.name = f\"{name}_Lid\"\r\n lid.rotation_euler.y = math.radians(90)\r\n bpy.ops.object.transform_apply(rotation=True)\r\n \r\n # Cut in half\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.mesh.bisect(\r\n plane_co=(0, 0, location[2] + body_height),\r\n plane_no=(0, 0, 1),\r\n clear_inner=True\r\n )\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n lid.data.materials.append(body_mat)\r\n \r\n # Set lid pivot at hinge\r\n bpy.context.scene.cursor.location = (\r\n location[0],\r\n location[1] - d/2,\r\n location[2] + body_height\r\n )\r\n bpy.ops.object.origin_set(type='ORIGIN_CURSOR')\r\n \r\n # Open lid\r\n lid.rotation_euler.x = math.radians(-lid_open)\r\n \r\n result['lid'] = lid\r\n \r\n # === BANDS ===\r\n if style in ['WOODEN', 'ORNATE']:\r\n bands = _create_chest_bands(w, d, body_height, location, name)\r\n result['bands'] = bands\r\n \r\n # === LOCK ===\r\n lock = _create_chest_lock(d, body_height, location, name)\r\n result['lock'] = lock\r\n \r\n # === TREASURE ===\r\n if with_treasure and lid_open > 30:\r\n treasure = _create_treasure_fill(w, d, body_height, location, name)\r\n result['treasure'] = treasure\r\n \r\n return result\r\n\r\n\r\ndef _create_chest_material(\r\n style: str,\r\n name: str,\r\n part: str\r\n) -> bpy.types.Material:\r\n \"\"\"Create chest material.\"\"\"\r\n mat = bpy.data.materials.new(f\"{name}_{part}Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n if style == 'WOODEN':\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.22, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n elif style == 'METAL':\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.35, 0.3, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n else: # ORNATE\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.2, 0.12, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n \r\n return mat\r\n\r\n\r\ndef _create_chest_bands(\r\n w: float, d: float, h: float,\r\n location: tuple, name: str\r\n) -> list:\r\n \"\"\"Create metal bands.\"\"\"\r\n bands = []\r\n band_mat = bpy.data.materials.new(f\"{name}_BandMat\")\r\n bsdf = band_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.28, 0.25, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n \r\n for i, x_pos in enumerate([-w*0.35, 0, w*0.35]):\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + x_pos,\r\n location[1],\r\n location[2] + h/2\r\n ))\r\n band = bpy.context.active_object\r\n band.name = f\"{name}_Band_{i}\"\r\n band.scale = (0.015, d/2 + 0.01, h/2 + 0.01)\r\n bpy.ops.object.transform_apply(scale=True)\r\n band.data.materials.append(band_mat)\r\n bands.append(band)\r\n \r\n return bands\r\n\r\n\r\ndef _create_chest_lock(\r\n d: float, h: float,\r\n location: tuple, name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create chest lock.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1] - d/2 - 0.01,\r\n location[2] + h * 0.7\r\n ))\r\n lock = bpy.context.active_object\r\n lock.name = f\"{name}_Lock\"\r\n lock.scale = (0.04, 0.015, 0.05)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n mat = bpy.data.materials.new(f\"{name}_LockMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.7, 0.6, 0.3, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.95\r\n bsdf.inputs['Roughness'].default_value = 0.25\r\n lock.data.materials.append(mat)\r\n \r\n return lock\r\n\r\n\r\ndef _create_treasure_fill(\r\n w: float, d: float, h: float,\r\n location: tuple, name: str\r\n) -> list:\r\n \"\"\"Create gold coins inside chest.\"\"\"\r\n import random\r\n items = []\r\n \r\n gold_mat = bpy.data.materials.new(f\"{name}_GoldMat\")\r\n bsdf = gold_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (1.0, 0.84, 0.0, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n bsdf.inputs['Roughness'].default_value = 0.2\r\n \r\n for i in range(15):\r\n pos = (\r\n location[0] + random.uniform(-w*0.35, w*0.35),\r\n location[1] + random.uniform(-d*0.35, d*0.35),\r\n location[2] + random.uniform(h*0.3, h*0.6)\r\n )\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.015,\r\n depth=0.003,\r\n location=pos\r\n )\r\n coin = bpy.context.active_object\r\n coin.name = f\"{name}_Coin_{i}\"\r\n coin.rotation_euler = (\r\n random.uniform(-0.5, 0.5),\r\n random.uniform(-0.5, 0.5),\r\n random.uniform(0, math.pi)\r\n )\r\n coin.data.materials.append(gold_mat)\r\n items.append(coin)\r\n \r\n return items\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_chest(lid_open=0, location=(0, 0, 0))\r\n create_chest(lid_open=60, with_treasure=True, location=(0.8, 0, 0))\r\n \r\n print(\"Created treasure chests\")","category":"tasks/modeling","source_file":"tasks\\modeling\\chest_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Coin Generator. Generates coins and coin stacks for game assets.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_coin(\r\n radius: float = 0.02,\r\n thickness: float = 0.003,\r\n material: str = 'GOLD',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Coin\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a coin.\r\n \r\n Args:\r\n radius: Coin radius\r\n thickness: Coin thickness\r\n material: 'GOLD', 'SILVER', 'BRONZE', 'COPPER'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The coin object\r\n \"\"\"\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius,\r\n depth=thickness,\r\n location=(location[0], location[1], location[2] + thickness/2)\r\n )\r\n coin = bpy.context.active_object\r\n coin.name = name\r\n \r\n # Add edge bevel for realism\r\n bpy.ops.object.modifier_add(type='BEVEL')\r\n coin.modifiers[\"Bevel\"].width = radius * 0.05\r\n coin.modifiers[\"Bevel\"].segments = 2\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n # Material colors\r\n colors = {\r\n 'GOLD': (1.0, 0.84, 0.0),\r\n 'SILVER': (0.8, 0.8, 0.85),\r\n 'BRONZE': (0.8, 0.5, 0.2),\r\n 'COPPER': (0.95, 0.64, 0.54)\r\n }\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n color = colors.get(material, colors['GOLD'])\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n bsdf.inputs['Roughness'].default_value = 0.25\r\n coin.data.materials.append(mat)\r\n \r\n return coin\r\n\r\n\r\ndef create_coin_stack(\r\n count: int = 8,\r\n radius: float = 0.02,\r\n material: str = 'GOLD',\r\n messy: bool = True,\r\n location: tuple = (0, 0, 0),\r\n seed: int = 42,\r\n name: str = \"CoinStack\"\r\n) -> list:\r\n \"\"\"\r\n Create a stack of coins.\r\n \r\n Args:\r\n count: Number of coins\r\n radius: Coin radius\r\n material: Metal type\r\n messy: Add randomness to positions\r\n location: Stack position\r\n seed: Random seed\r\n name: Stack name\r\n \r\n Returns:\r\n List of coin objects\r\n \"\"\"\r\n random.seed(seed)\r\n coins = []\r\n thickness = 0.003\r\n \r\n for i in range(count):\r\n offset_x = 0\r\n offset_y = 0\r\n rotation = 0\r\n \r\n if messy:\r\n offset_x = random.uniform(-radius * 0.2, radius * 0.2)\r\n offset_y = random.uniform(-radius * 0.2, radius * 0.2)\r\n rotation = random.uniform(-0.1, 0.1)\r\n \r\n pos = (\r\n location[0] + offset_x,\r\n location[1] + offset_y,\r\n location[2] + i * thickness\r\n )\r\n \r\n coin = create_coin(\r\n radius=radius,\r\n thickness=thickness,\r\n material=material,\r\n location=pos,\r\n name=f\"{name}_{i+1}\"\r\n )\r\n coin.rotation_euler.z = rotation\r\n coins.append(coin)\r\n \r\n return coins\r\n\r\n\r\ndef create_coin_pile(\r\n count: int = 20,\r\n spread: float = 0.1,\r\n material: str = 'GOLD',\r\n location: tuple = (0, 0, 0),\r\n seed: int = 123,\r\n name: str = \"CoinPile\"\r\n) -> list:\r\n \"\"\"Create a scattered pile of coins.\"\"\"\r\n random.seed(seed)\r\n coins = []\r\n thickness = 0.003\r\n radius = 0.02\r\n \r\n for i in range(count):\r\n angle = random.uniform(0, 2 * math.pi)\r\n dist = random.uniform(0, spread)\r\n \r\n pos = (\r\n location[0] + math.cos(angle) * dist,\r\n location[1] + math.sin(angle) * dist,\r\n location[2] + random.uniform(0, thickness * 5)\r\n )\r\n \r\n coin = create_coin(\r\n radius=radius * random.uniform(0.8, 1.0),\r\n material=material,\r\n location=pos,\r\n name=f\"{name}_{i+1}\"\r\n )\r\n \r\n # Random rotation for scattered look\r\n coin.rotation_euler = (\r\n random.uniform(-0.5, 0.5),\r\n random.uniform(-0.5, 0.5),\r\n random.uniform(0, math.pi * 2)\r\n )\r\n \r\n coins.append(coin)\r\n \r\n return coins\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_coin(material='GOLD', location=(0, 0, 0))\r\n create_coin_stack(count=10, location=(0.1, 0, 0))\r\n create_coin_pile(count=15, location=(0, 0.2, 0))\r\n \r\n print(\"Created coins\")","category":"tasks/modeling","source_file":"tasks\\modeling\\coin_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Crate and Box Generator. Generates wooden crates and cargo boxes.","output":"import bpy\r\nimport random\r\n\r\n\r\ndef create_crate(\r\n size: tuple = (0.5, 0.5, 0.5),\r\n style: str = 'WOODEN',\r\n plank_count: int = 4,\r\n worn: bool = False,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Crate\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a crate or box.\r\n \r\n Args:\r\n size: XYZ dimensions\r\n style: 'WOODEN', 'CARDBOARD', 'METAL'\r\n plank_count: Planks per side (wooden)\r\n worn: Add wear/damage look\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The crate object\r\n \"\"\"\r\n if style == 'WOODEN':\r\n crate = _create_wooden_crate(size, plank_count, location, name)\r\n elif style == 'CARDBOARD':\r\n crate = _create_cardboard_box(size, location, name)\r\n else: # METAL\r\n crate = _create_metal_crate(size, location, name)\r\n \r\n return crate\r\n\r\n\r\ndef _create_wooden_crate(\r\n size: tuple,\r\n plank_count: int,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create wooden crate with planks.\"\"\"\r\n plank_thickness = 0.02\r\n \r\n # Material\r\n wood_mat = bpy.data.materials.new(f\"{name}_WoodMat\")\r\n bsdf = wood_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.45, 0.32, 0.18, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n \r\n planks = []\r\n \r\n # Create planks for each face\r\n for face in ['front', 'back', 'left', 'right', 'top', 'bottom']:\r\n plank_size = _get_plank_dimensions(face, size, plank_count, plank_thickness)\r\n \r\n for i in range(plank_count):\r\n pos = _get_plank_position(face, size, i, plank_count, location)\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=pos)\r\n plank = bpy.context.active_object\r\n plank.scale = plank_size\r\n bpy.ops.object.transform_apply(scale=True)\r\n plank.data.materials.append(wood_mat)\r\n planks.append(plank)\r\n \r\n # Join all planks\r\n bpy.ops.object.select_all(action='DESELECT')\r\n for p in planks:\r\n p.select_set(True)\r\n bpy.context.view_layer.objects.active = planks[0]\r\n bpy.ops.object.join()\r\n \r\n crate = bpy.context.active_object\r\n crate.name = name\r\n \r\n return crate\r\n\r\n\r\ndef _get_plank_dimensions(\r\n face: str,\r\n size: tuple,\r\n plank_count: int,\r\n thickness: float\r\n) -> tuple:\r\n \"\"\"Get plank dimensions for face.\"\"\"\r\n w, d, h = size\r\n \r\n if face in ['front', 'back']:\r\n return (w/(plank_count*2), thickness/2, h/2)\r\n elif face in ['left', 'right']:\r\n return (thickness/2, d/2, h/(plank_count*2))\r\n else: # top/bottom\r\n return (w/2, d/(plank_count*2), thickness/2)\r\n\r\n\r\ndef _get_plank_position(\r\n face: str,\r\n size: tuple,\r\n index: int,\r\n count: int,\r\n location: tuple\r\n) -> tuple:\r\n \"\"\"Get plank position.\"\"\"\r\n w, d, h = size\r\n offset = (index + 0.5) / count - 0.5\r\n \r\n positions = {\r\n 'front': (location[0] + offset * w, location[1] - d/2, location[2] + h/2),\r\n 'back': (location[0] + offset * w, location[1] + d/2, location[2] + h/2),\r\n 'left': (location[0] - w/2, location[1], location[2] + h/2 + offset * h),\r\n 'right': (location[0] + w/2, location[1], location[2] + h/2 + offset * h),\r\n 'top': (location[0], location[1] + offset * d, location[2] + h),\r\n 'bottom': (location[0], location[1] + offset * d, location[2])\r\n }\r\n \r\n return positions[face]\r\n\r\n\r\ndef _create_cardboard_box(\r\n size: tuple,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create simple cardboard box.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0], location[1], location[2] + size[2]/2\r\n ))\r\n box = bpy.context.active_object\r\n box.name = name\r\n box.scale = (size[0]/2, size[1]/2, size[2]/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n mat = bpy.data.materials.new(f\"{name}_CardboardMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.5, 0.35, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n box.data.materials.append(mat)\r\n \r\n return box\r\n\r\n\r\ndef _create_metal_crate(\r\n size: tuple,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create metal shipping container.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0], location[1], location[2] + size[2]/2\r\n ))\r\n crate = bpy.context.active_object\r\n crate.name = name\r\n crate.scale = (size[0]/2, size[1]/2, size[2]/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n mat = bpy.data.materials.new(f\"{name}_MetalMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.35, 0.4, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n crate.data.materials.append(mat)\r\n \r\n return crate\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_crate(style='WOODEN', location=(0, 0, 0))\r\n create_crate(style='CARDBOARD', location=(0.8, 0, 0))\r\n create_crate(style='METAL', size=(0.6, 0.4, 0.4), location=(1.6, 0, 0))\r\n \r\n print(\"Created 3 crate types\")","category":"tasks/modeling","source_file":"tasks\\modeling\\crate_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Door Generator. Generates various door types with frames and handles.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_door(\r\n width: float = 0.9,\r\n height: float = 2.1,\r\n thickness: float = 0.04,\r\n style: str = 'PANEL',\r\n with_frame: bool = True,\r\n with_handle: bool = True,\r\n open_angle: float = 0,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Door\"\r\n) -> dict:\r\n \"\"\"\r\n Create a door with optional frame and handle.\r\n \r\n Args:\r\n width: Door width\r\n height: Door height\r\n thickness: Door thickness\r\n style: 'FLAT', 'PANEL', 'GLASS'\r\n with_frame: Add door frame\r\n with_handle: Add door handle\r\n open_angle: Opening angle in degrees\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with door parts\r\n \"\"\"\r\n result = {}\r\n \r\n # === DOOR PANEL ===\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] + height/2\r\n ))\r\n door = bpy.context.active_object\r\n door.name = name\r\n door.scale = (width/2, thickness/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Set origin to hinge side\r\n bpy.context.scene.cursor.location = (\r\n location[0] - width/2,\r\n location[1],\r\n location[2]\r\n )\r\n bpy.ops.object.origin_set(type='ORIGIN_CURSOR')\r\n \r\n # Apply opening angle\r\n door.rotation_euler.z = math.radians(open_angle)\r\n \r\n result['door'] = door\r\n \r\n # === PANEL DETAILS ===\r\n if style == 'PANEL':\r\n # Add beveled panels\r\n bpy.ops.object.modifier_add(type='BEVEL')\r\n door.modifiers[\"Bevel\"].width = 0.01\r\n door.modifiers[\"Bevel\"].segments = 2\r\n \r\n elif style == 'GLASS':\r\n # Add glass window\r\n window_height = height * 0.4\r\n window_y = height * 0.6\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1] - thickness * 0.3,\r\n location[2] + window_y\r\n ))\r\n window = bpy.context.active_object\r\n window.name = f\"{name}_Window\"\r\n window.scale = (width * 0.35, thickness * 0.1, window_height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Glass material\r\n glass_mat = bpy.data.materials.new(f\"{name}_GlassMat\")\r\n bsdf = glass_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Transmission Weight'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.05\r\n window.data.materials.append(glass_mat)\r\n \r\n result['window'] = window\r\n \r\n # === DOOR MATERIAL ===\r\n door_mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = door_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.25, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n door.data.materials.append(door_mat)\r\n \r\n # === FRAME ===\r\n if with_frame:\r\n frame = _create_door_frame(width, height, location, name)\r\n result['frame'] = frame\r\n \r\n # === HANDLE ===\r\n if with_handle:\r\n handle = _create_door_handle(width, height, thickness, location, name)\r\n result['handle'] = handle\r\n \r\n return result\r\n\r\n\r\ndef _create_door_frame(\r\n width: float,\r\n height: float,\r\n location: tuple,\r\n name: str\r\n) -> list:\r\n \"\"\"Create door frame.\"\"\"\r\n frame_width = 0.08\r\n frame_depth = 0.1\r\n frame_parts = []\r\n \r\n frame_mat = bpy.data.materials.new(f\"{name}_FrameMat\")\r\n bsdf = frame_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.9, 0.9, 0.88, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n \r\n # Top\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] + height + frame_width/2\r\n ))\r\n top = bpy.context.active_object\r\n top.name = f\"{name}_Frame_Top\"\r\n top.scale = ((width + frame_width*2)/2, frame_depth/2, frame_width/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n top.data.materials.append(frame_mat)\r\n frame_parts.append(top)\r\n \r\n # Sides\r\n for side, offset in [('L', -1), ('R', 1)]:\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + offset * (width/2 + frame_width/2),\r\n location[1],\r\n location[2] + height/2\r\n ))\r\n side_frame = bpy.context.active_object\r\n side_frame.name = f\"{name}_Frame_{side}\"\r\n side_frame.scale = (frame_width/2, frame_depth/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n side_frame.data.materials.append(frame_mat)\r\n frame_parts.append(side_frame)\r\n \r\n return frame_parts\r\n\r\n\r\ndef _create_door_handle(\r\n width: float,\r\n height: float,\r\n thickness: float,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create door handle.\"\"\"\r\n handle_height = height * 0.45\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.02,\r\n depth=0.08,\r\n location=(\r\n location[0] + width * 0.35,\r\n location[1] - thickness/2 - 0.04,\r\n location[2] + handle_height\r\n )\r\n )\r\n handle = bpy.context.active_object\r\n handle.name = f\"{name}_Handle\"\r\n handle.rotation_euler.x = math.radians(90)\r\n \r\n handle_mat = bpy.data.materials.new(f\"{name}_HandleMat\")\r\n bsdf = handle_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.55, 0.4, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.25\r\n handle.data.materials.append(handle_mat)\r\n \r\n return handle\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_door(style='FLAT', location=(0, 0, 0))\r\n create_door(style='PANEL', location=(1.5, 0, 0))\r\n create_door(style='GLASS', open_angle=45, location=(3, 0, 0))\r\n \r\n print(\"Created 3 door variations\")","category":"tasks/modeling","source_file":"tasks\\modeling\\door_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Fence Generator. Generates fence sections with posts and rails.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_fence(\r\n length: float = 5.0,\r\n height: float = 1.2,\r\n post_spacing: float = 2.0,\r\n style: str = 'PICKET',\r\n location: tuple = (0, 0, 0),\r\n direction: float = 0,\r\n name: str = \"Fence\"\r\n) -> dict:\r\n \"\"\"\r\n Create a fence section.\r\n \r\n Args:\r\n length: Total fence length\r\n height: Fence height\r\n post_spacing: Distance between posts\r\n style: 'PICKET', 'RAIL', 'CHAIN_LINK', 'PRIVACY'\r\n location: Start position\r\n direction: Direction angle in degrees\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with fence parts\r\n \"\"\"\r\n result = {}\r\n dir_rad = math.radians(direction)\r\n \r\n # Calculate number of posts\r\n num_posts = int(length / post_spacing) + 1\r\n actual_spacing = length / (num_posts - 1) if num_posts > 1 else 0\r\n \r\n # Materials\r\n post_mat = bpy.data.materials.new(f\"{name}_PostMat\")\r\n bsdf = post_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.45, 0.35, 0.2, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n \r\n # === POSTS ===\r\n posts = []\r\n post_size = 0.08\r\n \r\n for i in range(num_posts):\r\n dist = i * actual_spacing\r\n pos_x = location[0] + math.cos(dir_rad) * dist\r\n pos_y = location[1] + math.sin(dir_rad) * dist\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n pos_x, pos_y, location[2] + height/2\r\n ))\r\n post = bpy.context.active_object\r\n post.name = f\"{name}_Post_{i+1}\"\r\n post.scale = (post_size/2, post_size/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n post.data.materials.append(post_mat)\r\n posts.append(post)\r\n \r\n result['posts'] = posts\r\n \r\n # === STYLE-SPECIFIC ELEMENTS ===\r\n if style == 'PICKET':\r\n pickets = _create_picket_fence(\r\n length, height, post_spacing, location, dir_rad, name, post_mat\r\n )\r\n result['pickets'] = pickets\r\n \r\n elif style == 'RAIL':\r\n rails = _create_rail_fence(\r\n length, height, location, dir_rad, name, post_mat\r\n )\r\n result['rails'] = rails\r\n \r\n elif style == 'PRIVACY':\r\n boards = _create_privacy_fence(\r\n length, height, location, dir_rad, name, post_mat\r\n )\r\n result['boards'] = boards\r\n \r\n return result\r\n\r\n\r\ndef _create_picket_fence(\r\n length: float,\r\n height: float,\r\n post_spacing: float,\r\n location: tuple,\r\n dir_rad: float,\r\n name: str,\r\n material: bpy.types.Material\r\n) -> list:\r\n \"\"\"Create picket fence elements.\"\"\"\r\n pickets = []\r\n picket_spacing = 0.1\r\n picket_width = 0.07\r\n picket_height = height * 0.9\r\n \r\n num_pickets = int(length / picket_spacing)\r\n \r\n for i in range(num_pickets):\r\n dist = i * picket_spacing + picket_spacing/2\r\n pos_x = location[0] + math.cos(dir_rad) * dist\r\n pos_y = location[1] + math.sin(dir_rad) * dist\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n pos_x, pos_y, location[2] + picket_height/2\r\n ))\r\n picket = bpy.context.active_object\r\n picket.name = f\"{name}_Picket_{i+1}\"\r\n picket.scale = (picket_width/2, 0.015, picket_height/2)\r\n picket.rotation_euler.z = dir_rad\r\n bpy.ops.object.transform_apply(scale=True)\r\n picket.data.materials.append(material)\r\n pickets.append(picket)\r\n \r\n # Top rail\r\n rail_height = height * 0.8\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + math.cos(dir_rad) * length/2,\r\n location[1] + math.sin(dir_rad) * length/2,\r\n location[2] + rail_height\r\n ))\r\n rail = bpy.context.active_object\r\n rail.name = f\"{name}_TopRail\"\r\n rail.scale = (length/2, 0.03, 0.04)\r\n rail.rotation_euler.z = dir_rad\r\n bpy.ops.object.transform_apply(scale=True)\r\n rail.data.materials.append(material)\r\n pickets.append(rail)\r\n \r\n return pickets\r\n\r\n\r\ndef _create_rail_fence(\r\n length: float,\r\n height: float,\r\n location: tuple,\r\n dir_rad: float,\r\n name: str,\r\n material: bpy.types.Material\r\n) -> list:\r\n \"\"\"Create horizontal rail fence.\"\"\"\r\n rails = []\r\n rail_heights = [height * 0.25, height * 0.55, height * 0.85]\r\n \r\n for i, h in enumerate(rail_heights):\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.03,\r\n depth=length,\r\n location=(\r\n location[0] + math.cos(dir_rad) * length/2,\r\n location[1] + math.sin(dir_rad) * length/2,\r\n location[2] + h\r\n )\r\n )\r\n rail = bpy.context.active_object\r\n rail.name = f\"{name}_Rail_{i+1}\"\r\n rail.rotation_euler.y = math.pi/2\r\n rail.rotation_euler.z = dir_rad\r\n rail.data.materials.append(material)\r\n rails.append(rail)\r\n \r\n return rails\r\n\r\n\r\ndef _create_privacy_fence(\r\n length: float,\r\n height: float,\r\n location: tuple,\r\n dir_rad: float,\r\n name: str,\r\n material: bpy.types.Material\r\n) -> list:\r\n \"\"\"Create solid privacy fence boards.\"\"\"\r\n boards = []\r\n board_width = 0.15\r\n num_boards = int(length / board_width)\r\n \r\n for i in range(num_boards):\r\n dist = i * board_width + board_width/2\r\n pos_x = location[0] + math.cos(dir_rad) * dist\r\n pos_y = location[1] + math.sin(dir_rad) * dist\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n pos_x, pos_y, location[2] + height/2\r\n ))\r\n board = bpy.context.active_object\r\n board.name = f\"{name}_Board_{i+1}\"\r\n board.scale = (board_width/2 - 0.005, 0.02, height/2)\r\n board.rotation_euler.z = dir_rad\r\n bpy.ops.object.transform_apply(scale=True)\r\n board.data.materials.append(material)\r\n boards.append(board)\r\n \r\n return boards\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_fence(style='PICKET', location=(0, 0, 0))\r\n create_fence(style='RAIL', location=(0, 3, 0))\r\n create_fence(style='PRIVACY', location=(0, 6, 0))\r\n \r\n print(\"Created 3 fence styles\")","category":"tasks/modeling","source_file":"tasks\\modeling\\fence_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Food Props Generator. Generates simple food items for scene decoration.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_apple(\r\n radius: float = 0.04,\r\n color: tuple = (0.8, 0.15, 0.1),\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Apple\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create an apple.\"\"\"\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=radius,\r\n location=(location[0], location[1], location[2] + radius)\r\n )\r\n apple = bpy.context.active_object\r\n apple.name = name\r\n \r\n # Slight squish\r\n apple.scale.z = 0.85\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Dimple at top\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n for v in apple.data.vertices:\r\n if v.co.z > radius * 0.7:\r\n v.co.z -= (v.co.z - radius * 0.7) * 0.3\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n bsdf.inputs['Sheen Weight'].default_value = 0.3\r\n apple.data.materials.append(mat)\r\n \r\n # Stem\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.003,\r\n depth=0.015,\r\n location=(location[0], location[1], location[2] + radius * 1.65)\r\n )\r\n stem = bpy.context.active_object\r\n stem.name = f\"{name}_Stem\"\r\n \r\n stem_mat = bpy.data.materials.new(f\"{name}_StemMat\")\r\n bsdf = stem_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.2, 0.1, 1.0)\r\n stem.data.materials.append(stem_mat)\r\n stem.parent = apple\r\n \r\n return apple\r\n\r\n\r\ndef create_orange(\r\n radius: float = 0.045,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Orange\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create an orange.\"\"\"\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=radius,\r\n subdivisions=3,\r\n location=(location[0], location[1], location[2] + radius)\r\n )\r\n orange = bpy.context.active_object\r\n orange.name = name\r\n \r\n # Add bump\r\n bpy.ops.object.modifier_add(type='DISPLACE')\r\n tex = bpy.data.textures.new(f\"{name}_Bump\", 'NOISE')\r\n tex.noise_scale = 0.5\r\n orange.modifiers[\"Displace\"].texture = tex\r\n orange.modifiers[\"Displace\"].strength = 0.002\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (1.0, 0.5, 0.0, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n bsdf.inputs['Subsurface Weight'].default_value = 0.1\r\n orange.data.materials.append(mat)\r\n \r\n return orange\r\n\r\n\r\ndef create_banana(\r\n length: float = 0.18,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Banana\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a banana.\"\"\"\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.015,\r\n depth=length,\r\n location=(location[0], location[1], location[2] + 0.02)\r\n )\r\n banana = bpy.context.active_object\r\n banana.name = name\r\n banana.rotation_euler.y = math.radians(90)\r\n \r\n # Bend\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n banana.modifiers[\"SimpleDeform\"].deform_method = 'BEND'\r\n banana.modifiers[\"SimpleDeform\"].angle = math.radians(40)\r\n \r\n # Taper ends\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n banana.modifiers[\"SimpleDeform.001\"].deform_method = 'TAPER'\r\n banana.modifiers[\"SimpleDeform.001\"].factor = 0.3\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (1.0, 0.85, 0.2, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n banana.data.materials.append(mat)\r\n \r\n return banana\r\n\r\n\r\ndef create_bread_loaf(\r\n length: float = 0.25,\r\n width: float = 0.1,\r\n height: float = 0.08,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Bread\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a bread loaf.\"\"\"\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=1,\r\n location=(location[0], location[1], location[2] + height)\r\n )\r\n bread = bpy.context.active_object\r\n bread.name = name\r\n bread.scale = (length/2, width/2, height)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.7, 0.5, 0.3, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n bread.data.materials.append(mat)\r\n \r\n return bread\r\n\r\n\r\ndef create_fruit_bowl(\r\n location: tuple = (0, 0, 0),\r\n fruit_count: int = 5,\r\n name: str = \"FruitBowl\"\r\n) -> dict:\r\n \"\"\"Create a bowl with assorted fruit.\"\"\"\r\n result = {}\r\n \r\n # Bowl\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=0.15,\r\n location=(location[0], location[1], location[2] + 0.05)\r\n )\r\n bowl = bpy.context.active_object\r\n bowl.name = f\"{name}_Bowl\"\r\n bowl.scale.z = 0.4\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.bisect(plane_co=(0, 0, 0.06), plane_no=(0, 0, 1), clear_inner=True)\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n mat = bpy.data.materials.new(f\"{name}_BowlMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.9, 0.9, 0.85, 1.0)\r\n bowl.data.materials.append(mat)\r\n \r\n result['bowl'] = bowl\r\n \r\n # Add fruits\r\n fruits = []\r\n fruit_funcs = [create_apple, create_orange]\r\n \r\n for i in range(fruit_count):\r\n angle = (i / fruit_count) * 2 * math.pi\r\n r = random.uniform(0.03, 0.08)\r\n pos = (\r\n location[0] + math.cos(angle) * r,\r\n location[1] + math.sin(angle) * r,\r\n location[2] + 0.05\r\n )\r\n \r\n func = random.choice(fruit_funcs)\r\n fruit = func(location=pos, name=f\"{name}_Fruit_{i}\")\r\n fruits.append(fruit)\r\n \r\n result['fruits'] = fruits\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_apple(location=(0, 0, 0))\r\n create_orange(location=(0.1, 0, 0))\r\n create_banana(location=(0.2, 0, 0))\r\n create_fruit_bowl(location=(0, 0.3, 0))\r\n \r\n print(\"Created food items\")","category":"tasks/modeling","source_file":"tasks\\modeling\\food_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Gem and Crystal Generator. Generates gem and crystal shapes with refractive materials.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_gem(\r\n size: float = 0.05,\r\n style: str = 'ROUND',\r\n color: tuple = (0.1, 0.3, 0.9),\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Gem\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a gem/jewel.\r\n \r\n Args:\r\n size: Gem diameter\r\n style: 'ROUND', 'PRINCESS', 'EMERALD', 'OVAL'\r\n color: RGB gem color\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The gem object\r\n \"\"\"\r\n if style == 'ROUND':\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=size/2,\r\n subdivisions=2,\r\n location=location\r\n )\r\n gem = bpy.context.active_object\r\n gem.scale.z = 0.6\r\n \r\n elif style == 'PRINCESS':\r\n bpy.ops.mesh.primitive_cube_add(size=size, location=location)\r\n gem = bpy.context.active_object\r\n gem.scale.z = 0.7\r\n \r\n bpy.ops.object.modifier_add(type='BEVEL')\r\n gem.modifiers[\"Bevel\"].width = size * 0.15\r\n gem.modifiers[\"Bevel\"].segments = 2\r\n \r\n elif style == 'EMERALD':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n vertices=8,\r\n radius=size/2,\r\n depth=size * 0.7,\r\n location=location\r\n )\r\n gem = bpy.context.active_object\r\n \r\n bpy.ops.object.modifier_add(type='BEVEL')\r\n gem.modifiers[\"Bevel\"].width = size * 0.1\r\n \r\n else: # OVAL\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=size/2,\r\n location=location\r\n )\r\n gem = bpy.context.active_object\r\n gem.scale = (1, 0.7, 0.5)\r\n \r\n bpy.ops.object.transform_apply(scale=True)\r\n gem.name = name\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Glass material with color\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n mat.blend_method = 'BLEND'\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.0\r\n bsdf.inputs['IOR'].default_value = 2.4 # Diamond-like\r\n bsdf.inputs['Transmission Weight'].default_value = 1.0\r\n gem.data.materials.append(mat)\r\n \r\n return gem\r\n\r\n\r\ndef create_crystal(\r\n height: float = 0.15,\r\n base_radius: float = 0.03,\r\n facets: int = 6,\r\n color: tuple = (0.8, 0.4, 0.9),\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Crystal\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a crystal formation.\r\n \r\n Args:\r\n height: Crystal height\r\n base_radius: Base radius\r\n facets: Number of sides\r\n color: RGB color\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The crystal object\r\n \"\"\"\r\n bpy.ops.mesh.primitive_cone_add(\r\n vertices=facets,\r\n radius1=base_radius,\r\n radius2=0,\r\n depth=height,\r\n location=(location[0], location[1], location[2] + height/2)\r\n )\r\n crystal = bpy.context.active_object\r\n crystal.name = name\r\n \r\n # Add some variation\r\n bpy.ops.object.modifier_add(type='DISPLACE')\r\n tex = bpy.data.textures.new(f\"{name}_Disp\", 'NOISE')\r\n crystal.modifiers[\"Displace\"].texture = tex\r\n crystal.modifiers[\"Displace\"].strength = base_radius * 0.2\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n mat.blend_method = 'BLEND'\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.1\r\n bsdf.inputs['Transmission Weight'].default_value = 0.8\r\n bsdf.inputs['IOR'].default_value = 1.55\r\n \r\n # Emission for glow\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 0.5\r\n \r\n crystal.data.materials.append(mat)\r\n \r\n return crystal\r\n\r\n\r\ndef create_crystal_cluster(\r\n count: int = 5,\r\n max_height: float = 0.2,\r\n spread: float = 0.1,\r\n color: tuple = (0.6, 0.3, 0.9),\r\n location: tuple = (0, 0, 0),\r\n seed: int = 42,\r\n name: str = \"CrystalCluster\"\r\n) -> list:\r\n \"\"\"Create a cluster of crystals.\"\"\"\r\n random.seed(seed)\r\n crystals = []\r\n \r\n for i in range(count):\r\n angle = random.uniform(0, 2 * math.pi)\r\n dist = random.uniform(0, spread)\r\n \r\n pos = (\r\n location[0] + math.cos(angle) * dist,\r\n location[1] + math.sin(angle) * dist,\r\n location[2]\r\n )\r\n \r\n height = max_height * random.uniform(0.5, 1.0)\r\n radius = height * random.uniform(0.15, 0.25)\r\n \r\n # Slight color variation\r\n c_var = random.uniform(-0.1, 0.1)\r\n c = (\r\n max(0, min(1, color[0] + c_var)),\r\n max(0, min(1, color[1] + c_var)),\r\n max(0, min(1, color[2] + c_var))\r\n )\r\n \r\n crystal = create_crystal(\r\n height=height,\r\n base_radius=radius,\r\n color=c,\r\n location=pos,\r\n name=f\"{name}_{i+1}\"\r\n )\r\n \r\n # Tilt slightly\r\n crystal.rotation_euler.x = random.uniform(-0.2, 0.2)\r\n crystal.rotation_euler.y = random.uniform(-0.2, 0.2)\r\n \r\n crystals.append(crystal)\r\n \r\n return crystals\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_gem(style='ROUND', color=(0.9, 0.1, 0.1), location=(0, 0, 0))\r\n create_gem(style='PRINCESS', color=(0.1, 0.8, 0.2), location=(0.1, 0, 0))\r\n create_gem(style='EMERALD', color=(0.1, 0.3, 0.9), location=(0.2, 0, 0))\r\n \r\n create_crystal_cluster(location=(0, 0.3, 0))\r\n \r\n bpy.context.scene.render.engine = 'CYCLES'\r\n print(\"Created gems and crystals\")","category":"tasks/modeling","source_file":"tasks\\modeling\\gem_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Helmet Generator. Generates medieval and fantasy helmets.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_helmet(\r\n size: float = 0.25,\r\n style: str = 'KNIGHT',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Helmet\"\r\n) -> dict:\r\n \"\"\"\r\n Create a helmet.\r\n \r\n Args:\r\n size: Helmet size\r\n style: 'KNIGHT', 'VIKING', 'SPARTAN', 'BARBUTE'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with helmet parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Base dome\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=size/2,\r\n location=(location[0], location[1], location[2] + size/2)\r\n )\r\n helmet = bpy.context.active_object\r\n helmet.name = name\r\n \r\n # Cut bottom half\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.mesh.bisect(\r\n plane_co=(0, 0, location[2] + size * 0.2),\r\n plane_no=(0, 0, 1),\r\n clear_inner=True\r\n )\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n # Style-specific modifications\r\n if style == 'VIKING':\r\n _add_viking_features(helmet, size, location, name, result)\r\n elif style == 'SPARTAN':\r\n _add_spartan_features(helmet, size, location, name, result)\r\n elif style == 'KNIGHT':\r\n _add_knight_features(helmet, size, location, name, result)\r\n \r\n # Material\r\n metal_mat = bpy.data.materials.new(f\"{name}_MetalMat\")\r\n bsdf = metal_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.5, 0.5, 0.53, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.95\r\n bsdf.inputs['Roughness'].default_value = 0.35\r\n helmet.data.materials.append(metal_mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n result['helmet'] = helmet\r\n \r\n return result\r\n\r\n\r\ndef _add_viking_features(helmet, size, location, name, result):\r\n \"\"\"Add Viking helmet horns.\"\"\"\r\n horn_mat = bpy.data.materials.new(f\"{name}_HornMat\")\r\n bsdf = horn_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.75, 0.6, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n \r\n for side in [-1, 1]:\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=size * 0.08,\r\n radius2=0,\r\n depth=size * 0.5,\r\n location=(\r\n location[0] + side * size * 0.35,\r\n location[1],\r\n location[2] + size * 0.6\r\n )\r\n )\r\n horn = bpy.context.active_object\r\n horn.name = f\"{name}_Horn_{'L' if side < 0 else 'R'}\"\r\n horn.rotation_euler = (\r\n math.radians(-30),\r\n math.radians(side * 30),\r\n 0\r\n )\r\n horn.data.materials.append(horn_mat)\r\n\r\n\r\ndef _add_spartan_features(helmet, size, location, name, result):\r\n \"\"\"Add Spartan helmet crest.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] + size * 0.9\r\n ))\r\n crest = bpy.context.active_object\r\n crest.name = f\"{name}_Crest\"\r\n crest.scale = (0.02, size * 0.4, size * 0.4)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n crest_mat = bpy.data.materials.new(f\"{name}_CrestMat\")\r\n bsdf = crest_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.1, 0.1, 1.0)\r\n crest.data.materials.append(crest_mat)\r\n result['crest'] = crest\r\n\r\n\r\ndef _add_knight_features(helmet, size, location, name, result):\r\n \"\"\"Add knight helmet visor.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1] - size * 0.35,\r\n location[2] + size * 0.35\r\n ))\r\n visor = bpy.context.active_object\r\n visor.name = f\"{name}_Visor\"\r\n visor.scale = (size * 0.4, size * 0.15, size * 0.25)\r\n bpy.ops.object.transform_apply(scale=True)\r\n result['visor'] = visor\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_helmet(style='KNIGHT', location=(0, 0, 0))\r\n create_helmet(style='VIKING', location=(0.4, 0, 0))\r\n create_helmet(style='SPARTAN', location=(0.8, 0, 0))\r\n \r\n print(\"Created helmets\")","category":"tasks/modeling","source_file":"tasks\\modeling\\helmet_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Key Generator. Generates various key styles for game props.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_key(\r\n length: float = 0.08,\r\n style: str = 'SKELETON',\r\n material: str = 'BRASS',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Key\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a key.\r\n \r\n Args:\r\n length: Total key length\r\n style: 'SKELETON', 'MODERN', 'ORNATE', 'MASTER'\r\n material: 'BRASS', 'IRON', 'GOLD', 'SILVER'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The key object\r\n \"\"\"\r\n parts = []\r\n \r\n shaft_length = length * 0.6\r\n shaft_radius = length * 0.02\r\n \r\n # === SHAFT ===\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=shaft_radius,\r\n depth=shaft_length,\r\n location=(location[0] + shaft_length/2, location[1], location[2])\r\n )\r\n shaft = bpy.context.active_object\r\n shaft.rotation_euler.y = math.radians(90)\r\n parts.append(shaft)\r\n \r\n # === BOW (handle) ===\r\n bow_size = length * 0.35\r\n \r\n if style == 'SKELETON':\r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=bow_size * 0.5,\r\n minor_radius=shaft_radius * 1.5,\r\n location=(location[0] - bow_size * 0.3, location[1], location[2])\r\n )\r\n elif style == 'ORNATE':\r\n bpy.ops.mesh.primitive_circle_add(\r\n radius=bow_size * 0.5,\r\n fill_type='NGON',\r\n location=(location[0] - bow_size * 0.3, location[1], location[2])\r\n )\r\n bow = bpy.context.active_object\r\n bpy.ops.object.modifier_add(type='SOLIDIFY')\r\n bow.modifiers[\"Solidify\"].thickness = shaft_radius * 3\r\n elif style == 'MODERN':\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=bow_size,\r\n location=(location[0] - bow_size * 0.4, location[1], location[2])\r\n )\r\n bow = bpy.context.active_object\r\n bow.scale = (0.4, 0.1, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n else: # MASTER\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=bow_size * 0.4,\r\n location=(location[0] - bow_size * 0.3, location[1], location[2])\r\n )\r\n \r\n parts.append(bpy.context.active_object)\r\n \r\n # === BIT (teeth) ===\r\n bit_length = length * 0.15\r\n bit_height = length * 0.1\r\n \r\n if style in ['SKELETON', 'ORNATE', 'MASTER']:\r\n # Create teeth pattern\r\n teeth_count = 3 if style == 'SKELETON' else 4\r\n for i in range(teeth_count):\r\n tooth_h = bit_height * (0.6 + (i % 2) * 0.4)\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + shaft_length + bit_length * (i / teeth_count),\r\n location[1],\r\n location[2] - tooth_h/2\r\n ))\r\n tooth = bpy.context.active_object\r\n tooth.scale = (bit_length/teeth_count * 0.8, shaft_radius * 1.5, tooth_h/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n parts.append(tooth)\r\n else: # MODERN\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + shaft_length,\r\n location[1],\r\n location[2] - bit_height/2\r\n ))\r\n bit = bpy.context.active_object\r\n bit.scale = (bit_length, shaft_radius * 2, bit_height)\r\n bpy.ops.object.transform_apply(scale=True)\r\n parts.append(bit)\r\n \r\n # Join all parts\r\n bpy.ops.object.select_all(action='DESELECT')\r\n for p in parts:\r\n p.select_set(True)\r\n bpy.context.view_layer.objects.active = parts[0]\r\n bpy.ops.object.join()\r\n \r\n key = bpy.context.active_object\r\n key.name = name\r\n \r\n # Material\r\n mat = _create_key_material(material, name)\r\n key.data.materials.append(mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n \r\n return key\r\n\r\n\r\ndef _create_key_material(\r\n material: str,\r\n name: str\r\n) -> bpy.types.Material:\r\n \"\"\"Create key material.\"\"\"\r\n colors = {\r\n 'BRASS': (0.8, 0.7, 0.3),\r\n 'IRON': (0.4, 0.4, 0.42),\r\n 'GOLD': (1.0, 0.84, 0.0),\r\n 'SILVER': (0.8, 0.82, 0.85)\r\n }\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n color = colors.get(material, colors['BRASS'])\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.95\r\n bsdf.inputs['Roughness'].default_value = 0.3 if material == 'GOLD' else 0.4\r\n \r\n return mat\r\n\r\n\r\ndef create_key_ring(\r\n key_count: int = 3,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"KeyRing\"\r\n) -> dict:\r\n \"\"\"Create key ring with multiple keys.\"\"\"\r\n result = {}\r\n \r\n # Ring\r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=0.03,\r\n minor_radius=0.003,\r\n location=location\r\n )\r\n ring = bpy.context.active_object\r\n ring.name = f\"{name}_Ring\"\r\n \r\n ring_mat = bpy.data.materials.new(f\"{name}_RingMat\")\r\n bsdf = ring_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.5, 0.5, 0.52, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n ring.data.materials.append(ring_mat)\r\n \r\n result['ring'] = ring\r\n \r\n # Keys\r\n keys = []\r\n materials = ['BRASS', 'IRON', 'GOLD']\r\n styles = ['SKELETON', 'ORNATE', 'MASTER']\r\n \r\n for i in range(key_count):\r\n angle = (i / key_count) * math.pi * 0.5 - 0.3\r\n \r\n key = create_key(\r\n style=styles[i % len(styles)],\r\n material=materials[i % len(materials)],\r\n location=(location[0], location[1], location[2] - 0.04),\r\n name=f\"{name}_Key_{i+1}\"\r\n )\r\n key.rotation_euler.z = angle\r\n keys.append(key)\r\n \r\n result['keys'] = keys\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_key(style='SKELETON', location=(0, 0, 0))\r\n create_key(style='ORNATE', material='GOLD', location=(0, 0.1, 0))\r\n create_key_ring(key_count=3, location=(0.2, 0, 0))\r\n \r\n print(\"Created keys\")","category":"tasks/modeling","source_file":"tasks\\modeling\\key_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Ladder Generator. Generates ladders for environments and props.","output":"import bpy\r\n\r\n\r\ndef create_ladder(\r\n height: float = 2.0,\r\n width: float = 0.4,\r\n rung_spacing: float = 0.25,\r\n style: str = 'WOODEN',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Ladder\"\r\n) -> dict:\r\n \"\"\"\r\n Create a ladder.\r\n \r\n Args:\r\n height: Ladder height\r\n width: Ladder width\r\n rung_spacing: Distance between rungs\r\n style: 'WOODEN', 'METAL', 'ROPE'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with ladder parts\r\n \"\"\"\r\n result = {}\r\n parts = []\r\n \r\n rail_radius = 0.02 if style == 'METAL' else 0.025\r\n rung_radius = 0.015 if style == 'METAL' else 0.02\r\n \r\n # Materials\r\n if style == 'WOODEN':\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.3, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n elif style == 'METAL':\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.5, 0.5, 0.52, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n else: # ROPE\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.5, 0.35, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n \r\n # Side rails\r\n for side in [-1, 1]:\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=rail_radius,\r\n depth=height,\r\n location=(\r\n location[0] + side * width/2,\r\n location[1],\r\n location[2] + height/2\r\n )\r\n )\r\n rail = bpy.context.active_object\r\n rail.name = f\"{name}_Rail_{'L' if side < 0 else 'R'}\"\r\n rail.data.materials.append(mat)\r\n parts.append(rail)\r\n \r\n result['rails'] = parts[:2]\r\n \r\n # Rungs\r\n rungs = []\r\n rung_count = int(height / rung_spacing)\r\n \r\n for i in range(rung_count):\r\n rung_z = location[2] + rung_spacing * (i + 0.5)\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=rung_radius,\r\n depth=width,\r\n location=(location[0], location[1], rung_z)\r\n )\r\n rung = bpy.context.active_object\r\n rung.name = f\"{name}_Rung_{i}\"\r\n rung.rotation_euler.y = 1.5708\r\n rung.data.materials.append(mat)\r\n rungs.append(rung)\r\n parts.append(rung)\r\n \r\n result['rungs'] = rungs\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_ladder(style='WOODEN', location=(0, 0, 0))\r\n create_ladder(style='METAL', location=(0.6, 0, 0))\r\n \r\n print(\"Created ladders\")","category":"tasks/modeling","source_file":"tasks\\modeling\\ladder_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Lamp Generator. Generates various lamp types with working light sources.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_lamp(\r\n lamp_type: str = 'TABLE',\r\n height: float = 0.5,\r\n shade_color: tuple = (0.9, 0.85, 0.75),\r\n light_color: tuple = (1.0, 0.95, 0.85),\r\n light_power: float = 100,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Lamp\"\r\n) -> dict:\r\n \"\"\"\r\n Create a lamp with light source.\r\n \r\n Args:\r\n lamp_type: 'TABLE', 'FLOOR', 'DESK', 'PENDANT'\r\n height: Total lamp height\r\n shade_color: RGB shade color\r\n light_color: RGB light color\r\n light_power: Light intensity in watts\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with lamp parts\r\n \"\"\"\r\n result = {}\r\n \r\n if lamp_type == 'FLOOR':\r\n height = 1.6\r\n elif lamp_type == 'PENDANT':\r\n height = 0.3 # Just shade height\r\n \r\n # === BASE (not for pendant) ===\r\n if lamp_type != 'PENDANT':\r\n base_height = height * 0.08\r\n base_radius = height * 0.15\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=base_radius,\r\n depth=base_height,\r\n location=(location[0], location[1], location[2] + base_height/2)\r\n )\r\n base = bpy.context.active_object\r\n base.name = f\"{name}_Base\"\r\n result['base'] = base\r\n \r\n # Add metal material to base\r\n base_mat = bpy.data.materials.new(f\"{name}_BaseMat\")\r\n bsdf = base_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.15, 0.15, 0.18, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n bsdf.inputs['Roughness'].default_value = 0.3\r\n base.data.materials.append(base_mat)\r\n \r\n # === POLE ===\r\n if lamp_type != 'PENDANT':\r\n pole_radius = height * 0.015\r\n pole_height = height * 0.7\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=pole_radius,\r\n depth=pole_height,\r\n location=(location[0], location[1], location[2] + pole_height/2 + height * 0.08)\r\n )\r\n pole = bpy.context.active_object\r\n pole.name = f\"{name}_Pole\"\r\n \r\n pole_mat = bpy.data.materials.new(f\"{name}_PoleMat\")\r\n bsdf = pole_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.55, 0.4, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.25\r\n pole.data.materials.append(pole_mat)\r\n \r\n result['pole'] = pole\r\n \r\n # === SHADE ===\r\n shade_height = height * 0.25\r\n shade_radius_bottom = height * 0.18\r\n shade_radius_top = height * 0.12\r\n \r\n if lamp_type == 'PENDANT':\r\n shade_z = location[2]\r\n else:\r\n shade_z = location[2] + height - shade_height/2\r\n \r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=shade_radius_bottom,\r\n radius2=shade_radius_top,\r\n depth=shade_height,\r\n location=(location[0], location[1], shade_z)\r\n )\r\n shade = bpy.context.active_object\r\n shade.name = f\"{name}_Shade\"\r\n \r\n # Flip and hollow for shade\r\n shade.rotation_euler.x = math.radians(180)\r\n bpy.ops.object.transform_apply(rotation=True)\r\n \r\n # Shade material (slightly translucent)\r\n shade_mat = bpy.data.materials.new(f\"{name}_ShadeMat\")\r\n bsdf = shade_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*shade_color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n bsdf.inputs['Subsurface Weight'].default_value = 0.3\r\n shade.data.materials.append(shade_mat)\r\n \r\n result['shade'] = shade\r\n \r\n # === LIGHT SOURCE ===\r\n light_z = shade_z if lamp_type == 'PENDANT' else location[2] + height - shade_height * 0.6\r\n \r\n bpy.ops.object.light_add(\r\n type='POINT',\r\n location=(location[0], location[1], light_z)\r\n )\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = light_power\r\n light.data.color = light_color\r\n light.data.shadow_soft_size = shade_radius_bottom * 0.3\r\n \r\n result['light'] = light\r\n \r\n # === CORD (for pendant) ===\r\n if lamp_type == 'PENDANT':\r\n cord_length = 1.0\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.005,\r\n depth=cord_length,\r\n location=(location[0], location[1], location[2] + shade_height/2 + cord_length/2)\r\n )\r\n cord = bpy.context.active_object\r\n cord.name = f\"{name}_Cord\"\r\n result['cord'] = cord\r\n \r\n return result\r\n\r\n\r\ndef create_spotlight(\r\n location: tuple = (0, 0, 3),\r\n target: tuple = (0, 0, 0),\r\n power: float = 500,\r\n spot_size: float = 45,\r\n name: str = \"Spotlight\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a spotlight pointing at target.\"\"\"\r\n bpy.ops.object.light_add(type='SPOT', location=location)\r\n light = bpy.context.active_object\r\n light.name = name\r\n light.data.energy = power\r\n light.data.spot_size = math.radians(spot_size)\r\n light.data.spot_blend = 0.3\r\n \r\n # Point at target\r\n from mathutils import Vector\r\n direction = Vector(target) - Vector(location)\r\n light.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()\r\n \r\n return light\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_lamp('TABLE', location=(0, 0, 0))\r\n create_lamp('FLOOR', location=(2, 0, 0))\r\n create_lamp('PENDANT', location=(-2, 0, 2))\r\n \r\n print(\"Created 3 lamp variations\")","category":"tasks/modeling","source_file":"tasks\\modeling\\lamp_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Lantern Generator. Generates decorative lanterns with light sources.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_lantern(\r\n size: float = 0.15,\r\n style: str = 'CLASSIC',\r\n lit: bool = True,\r\n hanging: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Lantern\"\r\n) -> dict:\r\n \"\"\"\r\n Create a lantern.\r\n \r\n Args:\r\n size: Lantern size\r\n style: 'CLASSIC', 'PAPER', 'MODERN'\r\n lit: Add light source\r\n hanging: Add hanging hook\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with lantern parts\r\n \"\"\"\r\n result = {}\r\n \r\n if style == 'CLASSIC':\r\n result = _create_classic_lantern(size, location, name)\r\n elif style == 'PAPER':\r\n result = _create_paper_lantern(size, location, name)\r\n else:\r\n result = _create_modern_lantern(size, location, name)\r\n \r\n # Light source\r\n if lit:\r\n bpy.ops.object.light_add(\r\n type='POINT',\r\n location=(location[0], location[1], location[2])\r\n )\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 30\r\n light.data.color = (1.0, 0.8, 0.5)\r\n light.data.shadow_soft_size = size * 0.5\r\n result['light'] = light\r\n \r\n # Hanging hook\r\n if hanging:\r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=size * 0.15,\r\n minor_radius=size * 0.02,\r\n location=(location[0], location[1], location[2] + size * 0.7)\r\n )\r\n hook = bpy.context.active_object\r\n hook.name = f\"{name}_Hook\"\r\n result['hook'] = hook\r\n \r\n return result\r\n\r\n\r\ndef _create_classic_lantern(size, location, name):\r\n \"\"\"Create classic metal lantern.\"\"\"\r\n result = {}\r\n \r\n # Frame\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=size,\r\n location=location\r\n )\r\n frame = bpy.context.active_object\r\n frame.name = f\"{name}_Frame\"\r\n \r\n # Hollow out\r\n bpy.ops.object.modifier_add(type='WIREFRAME')\r\n frame.modifiers[\"Wireframe\"].thickness = size * 0.05\r\n \r\n frame_mat = bpy.data.materials.new(f\"{name}_FrameMat\")\r\n bsdf = frame_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.2, 0.18, 0.15, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n frame.data.materials.append(frame_mat)\r\n \r\n result['frame'] = frame\r\n \r\n # Glass panels\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=size * 0.9,\r\n location=location\r\n )\r\n glass = bpy.context.active_object\r\n glass.name = f\"{name}_Glass\"\r\n \r\n glass_mat = bpy.data.materials.new(f\"{name}_GlassMat\")\r\n glass_mat.blend_method = 'BLEND'\r\n bsdf = glass_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.9, 0.85, 0.7, 0.3)\r\n bsdf.inputs['Transmission Weight'].default_value = 0.9\r\n bsdf.inputs['Alpha'].default_value = 0.3\r\n glass.data.materials.append(glass_mat)\r\n \r\n result['glass'] = glass\r\n \r\n return result\r\n\r\n\r\ndef _create_paper_lantern(size, location, name):\r\n \"\"\"Create paper lantern.\"\"\"\r\n result = {}\r\n \r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=size * 0.6,\r\n location=location\r\n )\r\n lantern = bpy.context.active_object\r\n lantern.name = f\"{name}_Paper\"\r\n lantern.scale.z = 1.3\r\n bpy.ops.object.transform_apply(scale=True)\r\n bpy.ops.object.shade_smooth()\r\n \r\n mat = bpy.data.materials.new(f\"{name}_PaperMat\")\r\n mat.blend_method = 'BLEND'\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.9, 0.3, 0.2, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n bsdf.inputs['Emission Color'].default_value = (0.9, 0.4, 0.2, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 2.0\r\n lantern.data.materials.append(mat)\r\n \r\n result['paper'] = lantern\r\n \r\n return result\r\n\r\n\r\ndef _create_modern_lantern(size, location, name):\r\n \"\"\"Create modern geometric lantern.\"\"\"\r\n result = {}\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=size * 0.4,\r\n depth=size,\r\n location=location\r\n )\r\n body = bpy.context.active_object\r\n body.name = f\"{name}_Body\"\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.1, 0.1, 0.12, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n body.data.materials.append(mat)\r\n \r\n result['body'] = body\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_lantern(style='CLASSIC', location=(0, 0, 0))\r\n create_lantern(style='PAPER', location=(0.4, 0, 0))\r\n create_lantern(style='MODERN', location=(0.8, 0, 0))\r\n \r\n print(\"Created lanterns\")","category":"tasks/modeling","source_file":"tasks\\modeling\\lantern_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Potion Bottle Generator. Generates fantasy potion bottles with glowing liquids.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_potion(\r\n height: float = 0.12,\r\n style: str = 'ROUND',\r\n liquid_color: tuple = (0.2, 0.8, 0.3),\r\n liquid_level: float = 0.7,\r\n glow: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Potion\"\r\n) -> dict:\r\n \"\"\"\r\n Create a potion bottle.\r\n \r\n Args:\r\n height: Bottle height\r\n style: 'ROUND', 'FLASK', 'VIAL', 'HEART'\r\n liquid_color: RGB liquid color\r\n liquid_level: Fill amount (0-1)\r\n glow: Add emission to liquid\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with potion parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Style dimensions\r\n styles = {\r\n 'ROUND': {'body_h': 0.6, 'body_r': 0.35, 'neck_r': 0.1},\r\n 'FLASK': {'body_h': 0.5, 'body_r': 0.25, 'neck_r': 0.08},\r\n 'VIAL': {'body_h': 0.65, 'body_r': 0.15, 'neck_r': 0.1},\r\n 'HEART': {'body_h': 0.5, 'body_r': 0.3, 'neck_r': 0.08}\r\n }\r\n \r\n s = styles.get(style, styles['ROUND'])\r\n body_height = height * s['body_h']\r\n body_radius = height * s['body_r']\r\n neck_radius = height * s['neck_r']\r\n neck_height = height * 0.2\r\n \r\n # Bottle body\r\n if style == 'ROUND':\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=body_radius,\r\n location=(location[0], location[1], location[2] + body_radius)\r\n )\r\n else:\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=body_radius,\r\n depth=body_height,\r\n location=(location[0], location[1], location[2] + body_height/2)\r\n )\r\n \r\n body = bpy.context.active_object\r\n body.name = f\"{name}_Body\"\r\n \r\n # Neck\r\n neck_z = location[2] + body_height + neck_height/2\r\n if style == 'ROUND':\r\n neck_z = location[2] + body_radius * 1.6\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=neck_radius,\r\n depth=neck_height,\r\n location=(location[0], location[1], neck_z)\r\n )\r\n neck = bpy.context.active_object\r\n neck.name = f\"{name}_Neck\"\r\n \r\n # Join\r\n bpy.ops.object.select_all(action='DESELECT')\r\n body.select_set(True)\r\n neck.select_set(True)\r\n bpy.context.view_layer.objects.active = body\r\n bpy.ops.object.join()\r\n \r\n bottle = bpy.context.active_object\r\n bottle.name = f\"{name}_Bottle\"\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Glass material\r\n glass_mat = bpy.data.materials.new(f\"{name}_GlassMat\")\r\n glass_mat.blend_method = 'BLEND'\r\n bsdf = glass_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.95, 1.0, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.02\r\n bsdf.inputs['IOR'].default_value = 1.5\r\n bsdf.inputs['Transmission Weight'].default_value = 0.95\r\n bottle.data.materials.append(glass_mat)\r\n \r\n result['bottle'] = bottle\r\n \r\n # Liquid\r\n liquid = _create_potion_liquid(\r\n body_radius * 0.9,\r\n body_height * liquid_level,\r\n liquid_color,\r\n glow,\r\n (location[0], location[1], location[2]),\r\n name\r\n )\r\n result['liquid'] = liquid\r\n \r\n # Cork\r\n cork = _create_potion_cork(\r\n neck_radius,\r\n (location[0], location[1], neck_z + neck_height/2),\r\n name\r\n )\r\n result['cork'] = cork\r\n \r\n return result\r\n\r\n\r\ndef _create_potion_liquid(\r\n radius: float,\r\n height: float,\r\n color: tuple,\r\n glow: bool,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create potion liquid.\"\"\"\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius,\r\n depth=height,\r\n location=(location[0], location[1], location[2] + height/2)\r\n )\r\n liquid = bpy.context.active_object\r\n liquid.name = f\"{name}_Liquid\"\r\n \r\n mat = bpy.data.materials.new(f\"{name}_LiquidMat\")\r\n mat.blend_method = 'BLEND'\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.1\r\n bsdf.inputs['Transmission Weight'].default_value = 0.8\r\n bsdf.inputs['IOR'].default_value = 1.36\r\n \r\n if glow:\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 3.0\r\n \r\n liquid.data.materials.append(mat)\r\n bpy.ops.object.shade_smooth()\r\n \r\n return liquid\r\n\r\n\r\ndef _create_potion_cork(\r\n radius: float,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create cork stopper.\"\"\"\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius * 1.1,\r\n depth=radius * 1.5,\r\n location=(location[0], location[1], location[2] + radius * 0.3)\r\n )\r\n cork = bpy.context.active_object\r\n cork.name = f\"{name}_Cork\"\r\n \r\n mat = bpy.data.materials.new(f\"{name}_CorkMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.55, 0.45, 0.3, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n cork.data.materials.append(mat)\r\n \r\n return cork\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_potion(style='ROUND', liquid_color=(0.2, 0.9, 0.3), location=(0, 0, 0))\r\n create_potion(style='FLASK', liquid_color=(0.9, 0.2, 0.2), location=(0.15, 0, 0))\r\n create_potion(style='VIAL', liquid_color=(0.2, 0.4, 0.9), location=(0.25, 0, 0))\r\n \r\n bpy.context.scene.render.engine = 'CYCLES'\r\n print(\"Created potions\")","category":"tasks/modeling","source_file":"tasks\\modeling\\potion_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Low Poly Rock Generator. Generates low-poly rocks with random variations.","output":"import bpy\r\nimport random\r\nimport math\r\n\r\n\r\ndef create_rock(\r\n size: float = 1.0,\r\n detail: int = 2,\r\n roughness: float = 0.3,\r\n seed: int = 42,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Rock\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a procedural low-poly rock.\r\n \r\n Args:\r\n size: Base rock size\r\n detail: Subdivision level (1-3)\r\n roughness: Surface variation amount\r\n seed: Random seed\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n The created rock object\r\n \"\"\"\r\n random.seed(seed)\r\n \r\n # Start with icosphere\r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=size,\r\n subdivisions=detail,\r\n location=location\r\n )\r\n rock = bpy.context.active_object\r\n rock.name = name\r\n \r\n # Deform vertices randomly\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n for v in rock.data.vertices:\r\n displacement = random.uniform(-roughness, roughness) * size\r\n v.co.x += displacement * random.uniform(0.5, 1.5)\r\n v.co.y += displacement * random.uniform(0.5, 1.5)\r\n v.co.z += displacement * random.uniform(0.3, 1.0)\r\n \r\n # Flatten bottom slightly\r\n for v in rock.data.vertices:\r\n if v.co.z < -size * 0.3:\r\n v.co.z = -size * 0.3\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n gray = random.uniform(0.2, 0.5)\r\n bsdf.inputs['Base Color'].default_value = (gray, gray * 0.95, gray * 0.9, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n \r\n rock.data.materials.append(mat)\r\n \r\n # Shade flat for low-poly look\r\n bpy.ops.object.shade_flat()\r\n \r\n return rock\r\n\r\n\r\ndef create_rock_pile(\r\n count: int = 5,\r\n area_radius: float = 2.0,\r\n min_size: float = 0.3,\r\n max_size: float = 1.0,\r\n location: tuple = (0, 0, 0),\r\n seed: int = 123\r\n) -> list:\r\n \"\"\"Create a pile of rocks.\"\"\"\r\n random.seed(seed)\r\n rocks = []\r\n \r\n for i in range(count):\r\n angle = random.uniform(0, 2 * math.pi)\r\n dist = random.uniform(0, area_radius)\r\n size = random.uniform(min_size, max_size)\r\n \r\n pos = (\r\n location[0] + math.cos(angle) * dist,\r\n location[1] + math.sin(angle) * dist,\r\n location[2] + size * 0.3\r\n )\r\n \r\n rock = create_rock(\r\n size=size,\r\n roughness=random.uniform(0.2, 0.4),\r\n seed=seed + i,\r\n location=pos,\r\n name=f\"Rock_{i+1}\"\r\n )\r\n \r\n rock.rotation_euler = (\r\n random.uniform(0, 0.3),\r\n random.uniform(0, 0.3),\r\n random.uniform(0, math.pi * 2)\r\n )\r\n \r\n rocks.append(rock)\r\n \r\n return rocks\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_rock(size=1.5, location=(0, 0, 0))\r\n create_rock_pile(count=7, location=(5, 0, 0))\r\n \r\n print(\"Created rock and rock pile\")","category":"tasks/modeling","source_file":"tasks\\modeling\\rock_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Scroll Generator. Generates scrolls and parchment props.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_scroll(\r\n length: float = 0.25,\r\n width: float = 0.15,\r\n roll_radius: float = 0.015,\r\n unrolled: float = 0.5,\r\n with_ribbon: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Scroll\"\r\n) -> dict:\r\n \"\"\"\r\n Create a scroll.\r\n \r\n Args:\r\n length: Scroll length\r\n width: Paper width when unrolled\r\n roll_radius: Roll thickness\r\n unrolled: How much is unrolled (0-1)\r\n with_ribbon: Add decorative ribbon\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with scroll parts\r\n \"\"\"\r\n result = {}\r\n \r\n unrolled_length = width * unrolled\r\n rolled_length = width * (1 - unrolled) * 0.3\r\n \r\n # === UNROLLED PART ===\r\n if unrolled > 0.1:\r\n bpy.ops.mesh.primitive_plane_add(\r\n size=1,\r\n location=(location[0], location[1], location[2] + 0.001)\r\n )\r\n paper = bpy.context.active_object\r\n paper.name = f\"{name}_Paper\"\r\n paper.scale = (unrolled_length/2, length/2, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Slight curve\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n paper.modifiers[\"SimpleDeform\"].deform_method = 'BEND'\r\n paper.modifiers[\"SimpleDeform\"].angle = 0.1\r\n \r\n # Paper material\r\n paper_mat = bpy.data.materials.new(f\"{name}_PaperMat\")\r\n bsdf = paper_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.9, 0.85, 0.7, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n paper.data.materials.append(paper_mat)\r\n \r\n result['paper'] = paper\r\n \r\n # === ROLLS ===\r\n # Main roll\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=roll_radius,\r\n depth=length,\r\n location=(\r\n location[0] + unrolled_length/2 + roll_radius,\r\n location[1],\r\n location[2] + roll_radius\r\n )\r\n )\r\n roll = bpy.context.active_object\r\n roll.name = f\"{name}_Roll\"\r\n roll.rotation_euler.x = math.radians(90)\r\n \r\n roll_mat = bpy.data.materials.new(f\"{name}_RollMat\")\r\n bsdf = roll_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.85, 0.8, 0.65, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n roll.data.materials.append(roll_mat)\r\n \r\n result['roll'] = roll\r\n \r\n # Second roll if partially unrolled\r\n if unrolled > 0.1 and unrolled < 0.9:\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=roll_radius * 0.7,\r\n depth=length,\r\n location=(\r\n location[0] - unrolled_length/2 - roll_radius * 0.7,\r\n location[1],\r\n location[2] + roll_radius * 0.7\r\n )\r\n )\r\n roll2 = bpy.context.active_object\r\n roll2.name = f\"{name}_Roll2\"\r\n roll2.rotation_euler.x = math.radians(90)\r\n roll2.data.materials.append(roll_mat)\r\n result['roll2'] = roll2\r\n \r\n # === RIBBON ===\r\n if with_ribbon and unrolled < 0.3:\r\n ribbon = _create_scroll_ribbon(\r\n roll_radius, length, location, name\r\n )\r\n result['ribbon'] = ribbon\r\n \r\n return result\r\n\r\n\r\ndef _create_scroll_ribbon(\r\n radius: float,\r\n length: float,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create decorative ribbon.\"\"\"\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=radius * 1.3,\r\n depth=length * 0.1,\r\n location=(location[0], location[1], location[2] + radius)\r\n )\r\n ribbon = bpy.context.active_object\r\n ribbon.name = f\"{name}_Ribbon\"\r\n ribbon.rotation_euler.x = math.radians(90)\r\n \r\n mat = bpy.data.materials.new(f\"{name}_RibbonMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.1, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n ribbon.data.materials.append(mat)\r\n \r\n return ribbon\r\n\r\n\r\ndef create_book_page(\r\n width: float = 0.15,\r\n height: float = 0.2,\r\n curl: float = 0.1,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Page\"\r\n) -> bpy.types.Object:\r\n \"\"\"Create a single loose page.\"\"\"\r\n bpy.ops.mesh.primitive_plane_add(size=1, location=(\r\n location[0], location[1], location[2]\r\n ))\r\n page = bpy.context.active_object\r\n page.name = name\r\n page.scale = (width/2, height/2, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Subdivide for curl\r\n bpy.ops.object.modifier_add(type='SUBSURF')\r\n page.modifiers[\"Subdivision\"].levels = 3\r\n \r\n # Curl corners\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n page.modifiers[\"SimpleDeform\"].deform_method = 'BEND'\r\n page.modifiers[\"SimpleDeform\"].angle = curl\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.93, 0.88, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n page.data.materials.append(mat)\r\n \r\n return page\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_scroll(unrolled=0.0, location=(0, 0, 0))\r\n create_scroll(unrolled=0.7, location=(0.3, 0, 0))\r\n create_book_page(location=(0.6, 0, 0))\r\n \r\n print(\"Created scrolls and pages\")","category":"tasks/modeling","source_file":"tasks\\modeling\\scroll_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Shelf Unit Generator. Generates shelf units and bookcases with adjustable shelves.","output":"import bpy\r\n\r\n\r\ndef create_shelf_unit(\r\n width: float = 0.8,\r\n depth: float = 0.3,\r\n height: float = 1.8,\r\n shelf_count: int = 5,\r\n with_back: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"ShelfUnit\"\r\n) -> dict:\r\n \"\"\"\r\n Create a shelf unit/bookcase.\r\n \r\n Args:\r\n width: Unit width\r\n depth: Unit depth\r\n height: Total height\r\n shelf_count: Number of shelves (including top/bottom)\r\n with_back: Add back panel\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with shelf parts\r\n \"\"\"\r\n result = {}\r\n \r\n panel_thickness = 0.02\r\n \r\n # === SIDE PANELS ===\r\n sides = []\r\n for side, offset in [('L', -width/2 + panel_thickness/2), \r\n ('R', width/2 - panel_thickness/2)]:\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + offset,\r\n location[1],\r\n location[2] + height/2\r\n ))\r\n panel = bpy.context.active_object\r\n panel.name = f\"{name}_Side_{side}\"\r\n panel.scale = (panel_thickness/2, depth/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n sides.append(panel)\r\n \r\n result['sides'] = sides\r\n \r\n # === SHELVES ===\r\n shelves = []\r\n shelf_spacing = height / (shelf_count - 1)\r\n inner_width = width - panel_thickness * 2\r\n \r\n for i in range(shelf_count):\r\n shelf_z = location[2] + i * shelf_spacing + panel_thickness/2\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n shelf_z\r\n ))\r\n shelf = bpy.context.active_object\r\n shelf.name = f\"{name}_Shelf_{i+1}\"\r\n shelf.scale = (inner_width/2, depth/2, panel_thickness/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n shelves.append(shelf)\r\n \r\n result['shelves'] = shelves\r\n \r\n # === BACK PANEL ===\r\n if with_back:\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1] + depth/2 - panel_thickness/2,\r\n location[2] + height/2\r\n ))\r\n back = bpy.context.active_object\r\n back.name = f\"{name}_Back\"\r\n back.scale = (width/2, panel_thickness/2, height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n result['back'] = back\r\n \r\n # === MATERIAL ===\r\n mat = bpy.data.materials.new(f\"{name}_WoodMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.28, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n \r\n all_parts = sides + shelves\r\n if with_back:\r\n all_parts.append(result['back'])\r\n \r\n for part in all_parts:\r\n part.data.materials.append(mat)\r\n \r\n result['material'] = mat\r\n \r\n return result\r\n\r\n\r\ndef create_wall_shelf(\r\n width: float = 1.0,\r\n depth: float = 0.2,\r\n thickness: float = 0.03,\r\n bracket_style: str = 'HIDDEN',\r\n location: tuple = (0, 0, 1.5),\r\n name: str = \"WallShelf\"\r\n) -> dict:\r\n \"\"\"Create a floating wall shelf.\"\"\"\r\n result = {}\r\n \r\n # Shelf\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=location)\r\n shelf = bpy.context.active_object\r\n shelf.name = name\r\n shelf.scale = (width/2, depth/2, thickness/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n result['shelf'] = shelf\r\n \r\n # Brackets\r\n if bracket_style == 'L_BRACKET':\r\n for side, offset in [('L', -width/3), ('R', width/3)]:\r\n # Horizontal part\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + offset,\r\n location[1] + depth/4,\r\n location[2] - thickness\r\n ))\r\n h_bracket = bpy.context.active_object\r\n h_bracket.scale = (0.015, depth/4, 0.015)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Vertical part\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + offset,\r\n location[1] + depth/2 - 0.01,\r\n location[2] - thickness - 0.05\r\n ))\r\n v_bracket = bpy.context.active_object\r\n v_bracket.scale = (0.015, 0.015, 0.05)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.25, 0.12, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n shelf.data.materials.append(mat)\r\n \r\n result['material'] = mat\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_shelf_unit(location=(0, 0, 0))\r\n create_wall_shelf(location=(2, 0, 1.2), bracket_style='L_BRACKET')\r\n \r\n print(\"Created shelf unit and wall shelf\")","category":"tasks/modeling","source_file":"tasks\\modeling\\shelf_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Shield Generator. Generates various shield types for game/fantasy props.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_shield(\r\n size: float = 0.5,\r\n style: str = 'ROUND',\r\n thickness: float = 0.02,\r\n material_type: str = 'WOOD',\r\n emblem: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Shield\"\r\n) -> dict:\r\n \"\"\"\r\n Create a shield.\r\n \r\n Args:\r\n size: Shield size\r\n style: 'ROUND', 'KITE', 'HEATER', 'TOWER'\r\n thickness: Shield thickness\r\n material_type: 'WOOD', 'METAL', 'LEATHER'\r\n emblem: Add center emblem\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with shield parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Create base shape\r\n if style == 'ROUND':\r\n bpy.ops.mesh.primitive_circle_add(\r\n radius=size/2,\r\n fill_type='NGON',\r\n location=location\r\n )\r\n shield = bpy.context.active_object\r\n \r\n elif style == 'KITE':\r\n bpy.ops.mesh.primitive_cone_add(\r\n vertices=4,\r\n radius1=size * 0.4,\r\n radius2=0,\r\n depth=size,\r\n location=(location[0], location[1], location[2] - size * 0.2)\r\n )\r\n shield = bpy.context.active_object\r\n shield.rotation_euler.x = math.radians(90)\r\n bpy.ops.object.transform_apply(rotation=True)\r\n \r\n elif style == 'HEATER':\r\n bpy.ops.mesh.primitive_plane_add(size=size, location=location)\r\n shield = bpy.context.active_object\r\n # Make pointed at bottom\r\n for v in shield.data.vertices:\r\n if v.co.y < 0:\r\n v.co.y *= 0.6\r\n v.co.x *= 1 + v.co.y\r\n \r\n else: # TOWER\r\n bpy.ops.mesh.primitive_plane_add(size=1, location=location)\r\n shield = bpy.context.active_object\r\n shield.scale = (size * 0.5, size, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n shield.name = name\r\n \r\n # Rotate to stand upright\r\n shield.rotation_euler.x = math.radians(90)\r\n \r\n # Add thickness\r\n bpy.ops.object.modifier_add(type='SOLIDIFY')\r\n shield.modifiers[\"Solidify\"].thickness = thickness\r\n \r\n # Slight curve\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n shield.modifiers[\"SimpleDeform\"].deform_method = 'BEND'\r\n shield.modifiers[\"SimpleDeform\"].angle = 0.3\r\n shield.modifiers[\"SimpleDeform\"].deform_axis = 'X'\r\n \r\n # Material\r\n mat = _create_shield_material(material_type, name)\r\n shield.data.materials.append(mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n result['shield'] = shield\r\n \r\n # Emblem/Boss\r\n if emblem and style == 'ROUND':\r\n boss = _create_shield_boss(size * 0.15, location, name)\r\n result['boss'] = boss\r\n \r\n # Handle on back\r\n handle = _create_shield_handle(size, thickness, location, name)\r\n result['handle'] = handle\r\n \r\n return result\r\n\r\n\r\ndef _create_shield_material(\r\n material_type: str,\r\n name: str\r\n) -> bpy.types.Material:\r\n \"\"\"Create shield material.\"\"\"\r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n if material_type == 'WOOD':\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.28, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n elif material_type == 'METAL':\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.6, 0.65, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.35\r\n else: # LEATHER\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.2, 0.12, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n \r\n return mat\r\n\r\n\r\ndef _create_shield_boss(\r\n radius: float,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create center boss/emblem.\"\"\"\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=radius,\r\n location=(location[0], location[1] - 0.02, location[2])\r\n )\r\n boss = bpy.context.active_object\r\n boss.name = f\"{name}_Boss\"\r\n boss.scale.y = 0.5\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n mat = bpy.data.materials.new(f\"{name}_BossMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.7, 0.65, 0.4, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.3\r\n boss.data.materials.append(mat)\r\n \r\n return boss\r\n\r\n\r\ndef _create_shield_handle(\r\n size: float,\r\n thickness: float,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create handle on back of shield.\"\"\"\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.015,\r\n depth=size * 0.3,\r\n location=(location[0], location[1] + thickness + 0.03, location[2])\r\n )\r\n handle = bpy.context.active_object\r\n handle.name = f\"{name}_Handle\"\r\n handle.rotation_euler.z = math.radians(90)\r\n \r\n mat = bpy.data.materials.new(f\"{name}_HandleMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.15, 0.1, 0.05, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n handle.data.materials.append(mat)\r\n \r\n return handle\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_shield(style='ROUND', location=(0, 0, 0))\r\n create_shield(style='KITE', location=(0.7, 0, 0))\r\n create_shield(style='HEATER', material_type='METAL', location=(1.4, 0, 0))\r\n \r\n print(\"Created shields\")","category":"tasks/modeling","source_file":"tasks\\modeling\\shield_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Staff and Wand Generator. Generates magical staffs and wands.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_staff(\r\n length: float = 1.5,\r\n style: str = 'WIZARD',\r\n with_crystal: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Staff\"\r\n) -> dict:\r\n \"\"\"\r\n Create a magical staff.\r\n \r\n Args:\r\n length: Staff length\r\n style: 'WIZARD', 'DRUID', 'DARK'\r\n with_crystal: Add crystal at top\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with staff parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Main shaft\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.025,\r\n depth=length,\r\n location=(location[0], location[1], location[2] + length/2)\r\n )\r\n shaft = bpy.context.active_object\r\n shaft.name = f\"{name}_Shaft\"\r\n \r\n # Slight taper\r\n for v in shaft.data.vertices:\r\n if v.co.z > 0:\r\n factor = 1 - (v.co.z / (length/2)) * 0.3\r\n v.co.x *= factor\r\n v.co.y *= factor\r\n \r\n # Style-specific modifications\r\n if style == 'DRUID':\r\n # Add knotty texture\r\n bpy.ops.object.modifier_add(type='DISPLACE')\r\n tex = bpy.data.textures.new(f\"{name}_Knots\", 'NOISE')\r\n shaft.modifiers[\"Displace\"].texture = tex\r\n shaft.modifiers[\"Displace\"].strength = 0.01\r\n \r\n shaft_mat = bpy.data.materials.new(f\"{name}_ShaftMat\")\r\n bsdf = shaft_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n \r\n if style == 'DARK':\r\n bsdf.inputs['Base Color'].default_value = (0.1, 0.08, 0.08, 1.0)\r\n else:\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.25, 0.12, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.7\r\n shaft.data.materials.append(shaft_mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n result['shaft'] = shaft\r\n \r\n # Crystal/Orb\r\n if with_crystal:\r\n crystal = _create_staff_crystal(\r\n length, style, location, name\r\n )\r\n result['crystal'] = crystal\r\n \r\n # Head ornament\r\n head = _create_staff_head(length, style, location, name)\r\n result['head'] = head\r\n \r\n return result\r\n\r\n\r\ndef _create_staff_crystal(\r\n length: float,\r\n style: str,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create crystal at staff top.\"\"\"\r\n crystal_pos = (location[0], location[1], location[2] + length + 0.05)\r\n \r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=0.04,\r\n subdivisions=2,\r\n location=crystal_pos\r\n )\r\n crystal = bpy.context.active_object\r\n crystal.name = f\"{name}_Crystal\"\r\n \r\n colors = {\r\n 'WIZARD': (0.2, 0.5, 1.0),\r\n 'DRUID': (0.2, 0.8, 0.3),\r\n 'DARK': (0.5, 0.1, 0.5)\r\n }\r\n \r\n mat = bpy.data.materials.new(f\"{name}_CrystalMat\")\r\n mat.blend_method = 'BLEND'\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n color = colors.get(style, colors['WIZARD'])\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Transmission Weight'].default_value = 0.8\r\n bsdf.inputs['Emission Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 3.0\r\n crystal.data.materials.append(mat)\r\n \r\n bpy.ops.object.shade_smooth()\r\n return crystal\r\n\r\n\r\ndef _create_staff_head(\r\n length: float,\r\n style: str,\r\n location: tuple,\r\n name: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create staff head/holder.\"\"\"\r\n bpy.ops.mesh.primitive_torus_add(\r\n major_radius=0.05,\r\n minor_radius=0.01,\r\n location=(location[0], location[1], location[2] + length)\r\n )\r\n head = bpy.context.active_object\r\n head.name = f\"{name}_Head\"\r\n \r\n mat = bpy.data.materials.new(f\"{name}_HeadMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.5, 0.3, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n head.data.materials.append(mat)\r\n \r\n return head\r\n\r\n\r\ndef create_wand(\r\n length: float = 0.35,\r\n core_glow: tuple = (0.8, 0.6, 0.2),\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Wand\"\r\n) -> dict:\r\n \"\"\"\r\n Create a magic wand.\r\n \r\n Args:\r\n length: Wand length\r\n core_glow: RGB glow color\r\n location: Position\r\n name: Object name\r\n \"\"\"\r\n result = {}\r\n \r\n # Wand body\r\n bpy.ops.mesh.primitive_cone_add(\r\n vertices=8,\r\n radius1=0.015,\r\n radius2=0.008,\r\n depth=length,\r\n location=(location[0], location[1], location[2] + length/2)\r\n )\r\n wand = bpy.context.active_object\r\n wand.name = name\r\n \r\n mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.25, 0.18, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n wand.data.materials.append(mat)\r\n \r\n result['wand'] = wand\r\n \r\n # Handle decoration\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=0.018,\r\n location=(location[0], location[1], location[2] + 0.01)\r\n )\r\n pommel = bpy.context.active_object\r\n pommel.name = f\"{name}_Pommel\"\r\n pommel.data.materials.append(mat)\r\n result['pommel'] = pommel\r\n \r\n # Tip glow\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=0.01,\r\n location=(location[0], location[1], location[2] + length + 0.005)\r\n )\r\n tip = bpy.context.active_object\r\n tip.name = f\"{name}_Tip\"\r\n \r\n tip_mat = bpy.data.materials.new(f\"{name}_TipMat\")\r\n bsdf = tip_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Emission Color'].default_value = (*core_glow, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 5.0\r\n tip.data.materials.append(tip_mat)\r\n result['tip'] = tip\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_staff(style='WIZARD', location=(0, 0, 0))\r\n create_staff(style='DRUID', location=(0.3, 0, 0))\r\n create_wand(location=(0.6, 0, 0))\r\n \r\n bpy.context.scene.render.engine = 'CYCLES'\r\n print(\"Created staffs and wand\")","category":"tasks/modeling","source_file":"tasks\\modeling\\staff_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Stairs Generator. Generates straight and spiral staircases.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_stairs(\r\n steps: int = 12,\r\n step_width: float = 1.0,\r\n step_depth: float = 0.28,\r\n step_height: float = 0.18,\r\n style: str = 'STRAIGHT',\r\n with_railings: bool = True,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Stairs\"\r\n) -> dict:\r\n \"\"\"\r\n Create a staircase.\r\n \r\n Args:\r\n steps: Number of steps\r\n step_width: Width of steps\r\n step_depth: Depth/tread of steps\r\n step_height: Height/rise of steps\r\n style: 'STRAIGHT', 'L_TURN', 'U_TURN'\r\n with_railings: Add handrails\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with staircase parts\r\n \"\"\"\r\n result = {}\r\n step_objs = []\r\n \r\n # Material\r\n step_mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = step_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.4, 0.3, 0.2, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n \r\n for i in range(steps):\r\n # Position\r\n x = location[0]\r\n y = location[1] - i * step_depth\r\n z = location[2] + i * step_height\r\n \r\n # Step tread\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n x, y - step_depth/2, z + step_height/2\r\n ))\r\n step = bpy.context.active_object\r\n step.name = f\"{name}_Step_{i+1}\"\r\n step.scale = (step_width/2, step_depth/2, step_height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n step.data.materials.append(step_mat)\r\n step_objs.append(step)\r\n \r\n result['steps'] = step_objs\r\n \r\n # === STRINGERS (side supports) ===\r\n total_run = steps * step_depth\r\n total_rise = steps * step_height\r\n stringer_length = math.sqrt(total_run**2 + total_rise**2)\r\n stringer_angle = math.atan2(total_rise, total_run)\r\n \r\n for side, offset in [('L', -step_width/2 - 0.03), ('R', step_width/2 + 0.03)]:\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + offset,\r\n location[1] - total_run/2,\r\n location[2] + total_rise/2\r\n ))\r\n stringer = bpy.context.active_object\r\n stringer.name = f\"{name}_Stringer_{side}\"\r\n stringer.scale = (0.03, stringer_length/2, 0.15)\r\n stringer.rotation_euler.x = -stringer_angle\r\n bpy.ops.object.transform_apply(scale=True)\r\n stringer.data.materials.append(step_mat)\r\n \r\n # === RAILINGS ===\r\n if with_railings:\r\n railings = _create_stair_railings(\r\n steps, step_width, step_depth, step_height,\r\n location, name\r\n )\r\n result['railings'] = railings\r\n \r\n return result\r\n\r\n\r\ndef _create_stair_railings(\r\n steps: int,\r\n step_width: float,\r\n step_depth: float,\r\n step_height: float,\r\n location: tuple,\r\n name: str\r\n) -> list:\r\n \"\"\"Create stair railings.\"\"\"\r\n railings = []\r\n railing_height = 0.9\r\n \r\n rail_mat = bpy.data.materials.new(f\"{name}_RailMat\")\r\n bsdf = rail_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.2, 0.18, 0.15, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n \r\n total_run = steps * step_depth\r\n total_rise = steps * step_height\r\n stringer_angle = math.atan2(total_rise, total_run)\r\n \r\n for side, offset in [('L', -step_width/2), ('R', step_width/2)]:\r\n # Handrail\r\n rail_length = math.sqrt(total_run**2 + total_rise**2)\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.025,\r\n depth=rail_length,\r\n location=(\r\n location[0] + offset,\r\n location[1] - total_run/2,\r\n location[2] + total_rise/2 + railing_height\r\n )\r\n )\r\n rail = bpy.context.active_object\r\n rail.name = f\"{name}_Rail_{side}\"\r\n rail.rotation_euler.x = math.pi/2 - stringer_angle\r\n rail.data.materials.append(rail_mat)\r\n railings.append(rail)\r\n \r\n # Posts\r\n post_interval = 3\r\n for i in range(0, steps, post_interval):\r\n y = location[1] - i * step_depth\r\n z = location[2] + i * step_height\r\n \r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.02,\r\n depth=railing_height,\r\n location=(offset + location[0], y, z + railing_height/2)\r\n )\r\n post = bpy.context.active_object\r\n post.name = f\"{name}_Post_{side}_{i}\"\r\n post.data.materials.append(rail_mat)\r\n railings.append(post)\r\n \r\n return railings\r\n\r\n\r\ndef create_spiral_stairs(\r\n steps: int = 16,\r\n inner_radius: float = 0.3,\r\n outer_radius: float = 1.2,\r\n total_height: float = 3.0,\r\n total_rotation: float = 360,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"SpiralStairs\"\r\n) -> dict:\r\n \"\"\"Create spiral staircase.\"\"\"\r\n result = {}\r\n step_objs = []\r\n \r\n step_mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bsdf = step_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.3, 0.35, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n bsdf.inputs['Roughness'].default_value = 0.3\r\n \r\n step_height = total_height / steps\r\n angle_per_step = math.radians(total_rotation) / steps\r\n \r\n for i in range(steps):\r\n angle = i * angle_per_step\r\n z = location[2] + i * step_height\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + math.cos(angle) * (inner_radius + outer_radius) / 2,\r\n location[1] + math.sin(angle) * (inner_radius + outer_radius) / 2,\r\n z\r\n ))\r\n step = bpy.context.active_object\r\n step.name = f\"{name}_Step_{i+1}\"\r\n step.scale = ((outer_radius - inner_radius)/2, 0.15, step_height/2)\r\n step.rotation_euler.z = angle\r\n bpy.ops.object.transform_apply(scale=True)\r\n step.data.materials.append(step_mat)\r\n step_objs.append(step)\r\n \r\n result['steps'] = step_objs\r\n \r\n # Center pole\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=inner_radius * 0.8,\r\n depth=total_height,\r\n location=(location[0], location[1], location[2] + total_height/2)\r\n )\r\n pole = bpy.context.active_object\r\n pole.name = f\"{name}_Pole\"\r\n pole.data.materials.append(step_mat)\r\n result['pole'] = pole\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_stairs(steps=10, location=(0, 0, 0))\r\n create_spiral_stairs(steps=16, location=(3, 0, 0))\r\n \r\n print(\"Created straight and spiral stairs\")","category":"tasks/modeling","source_file":"tasks\\modeling\\stairs_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Low Poly Sword Generator. Generates low-poly swords with customizable blade and handle.","output":"import bpy\r\n\r\n\r\ndef create_sword(\r\n blade_length: float = 1.0,\r\n blade_width: float = 0.1,\r\n handle_length: float = 0.25,\r\n guard_width: float = 0.2,\r\n style: str = 'LONGSWORD',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Sword\"\r\n) -> dict:\r\n \"\"\"\r\n Create a low-poly sword.\r\n \r\n Args:\r\n blade_length: Length of blade\r\n blade_width: Width at base of blade\r\n handle_length: Handle/grip length\r\n guard_width: Crossguard width\r\n style: 'LONGSWORD', 'DAGGER', 'KATANA', 'RAPIER'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with sword parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Style adjustments\r\n if style == 'DAGGER':\r\n blade_length *= 0.4\r\n handle_length *= 0.7\r\n guard_width *= 0.6\r\n elif style == 'KATANA':\r\n blade_width *= 0.6\r\n guard_width *= 0.4\r\n elif style == 'RAPIER':\r\n blade_width *= 0.4\r\n blade_length *= 1.2\r\n guard_width *= 1.2\r\n \r\n # === BLADE ===\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] + handle_length + blade_length/2\r\n ))\r\n blade = bpy.context.active_object\r\n blade.name = f\"{name}_Blade\"\r\n blade.scale = (blade_width/2, 0.01, blade_length/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Taper blade to point\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='DESELECT')\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n # Select top vertices and scale to point\r\n for v in blade.data.vertices:\r\n if v.co.z > blade_length/2 - 0.01:\r\n v.co.x = 0\r\n \r\n result['blade'] = blade\r\n \r\n # Blade material\r\n blade_mat = bpy.data.materials.new(f\"{name}_BladeMat\")\r\n bsdf = blade_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.82, 0.85, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 1.0\r\n bsdf.inputs['Roughness'].default_value = 0.2\r\n blade.data.materials.append(blade_mat)\r\n \r\n # === GUARD (Crossguard) ===\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] + handle_length - 0.01\r\n ))\r\n guard = bpy.context.active_object\r\n guard.name = f\"{name}_Guard\"\r\n guard.scale = (guard_width/2, 0.02, 0.02)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n guard_mat = bpy.data.materials.new(f\"{name}_GuardMat\")\r\n bsdf = guard_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.6, 0.5, 0.2, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.3\r\n guard.data.materials.append(guard_mat)\r\n \r\n result['guard'] = guard\r\n \r\n # === HANDLE ===\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=0.02,\r\n depth=handle_length,\r\n location=(\r\n location[0],\r\n location[1],\r\n location[2] + handle_length/2\r\n )\r\n )\r\n handle = bpy.context.active_object\r\n handle.name = f\"{name}_Handle\"\r\n \r\n handle_mat = bpy.data.materials.new(f\"{name}_HandleMat\")\r\n bsdf = handle_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.15, 0.08, 0.02, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n handle.data.materials.append(handle_mat)\r\n \r\n result['handle'] = handle\r\n \r\n # === POMMEL ===\r\n bpy.ops.mesh.primitive_uv_sphere_add(\r\n radius=0.03,\r\n location=(location[0], location[1], location[2])\r\n )\r\n pommel = bpy.context.active_object\r\n pommel.name = f\"{name}_Pommel\"\r\n pommel.data.materials.append(guard_mat)\r\n \r\n result['pommel'] = pommel\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_sword(style='LONGSWORD', location=(0, 0, 0))\r\n create_sword(style='DAGGER', location=(0.5, 0, 0))\r\n create_sword(style='KATANA', location=(-0.5, 0, 0))\r\n \r\n print(\"Created 3 sword variations\")","category":"tasks/modeling","source_file":"tasks\\modeling\\sword_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Procedural Table Generator. Generates various table types with customizable dimensions.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_table(\r\n table_type: str = 'DINING',\r\n width: float = 1.5,\r\n depth: float = 0.9,\r\n height: float = 0.75,\r\n leg_style: str = 'SQUARE',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Table\"\r\n) -> dict:\r\n \"\"\"\r\n Create a procedural table.\r\n \r\n Args:\r\n table_type: 'DINING', 'COFFEE', 'DESK', 'ROUND'\r\n width: Table width (X)\r\n depth: Table depth (Y)\r\n height: Table height\r\n leg_style: 'SQUARE', 'ROUND', 'TAPERED'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with table parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Adjust for table type\r\n if table_type == 'COFFEE':\r\n height = 0.45\r\n elif table_type == 'DESK':\r\n depth = 0.7\r\n elif table_type == 'ROUND':\r\n depth = width # Make circular\r\n \r\n top_thickness = 0.04\r\n \r\n # === TABLE TOP ===\r\n if table_type == 'ROUND':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=width/2,\r\n depth=top_thickness,\r\n location=(location[0], location[1], location[2] + height - top_thickness/2)\r\n )\r\n else:\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0], location[1], location[2] + height - top_thickness/2\r\n ))\r\n bpy.context.active_object.scale = (width/2, depth/2, top_thickness/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n top = bpy.context.active_object\r\n top.name = f\"{name}_Top\"\r\n result['top'] = top\r\n \r\n # === LEGS ===\r\n leg_inset = 0.08\r\n leg_size = 0.05\r\n leg_height = height - top_thickness\r\n \r\n if table_type == 'ROUND':\r\n # 4 legs in circular pattern\r\n leg_positions = [\r\n (math.cos(a) * (width/2 - leg_inset), math.sin(a) * (width/2 - leg_inset))\r\n for a in [math.pi/4, 3*math.pi/4, 5*math.pi/4, 7*math.pi/4]\r\n ]\r\n else:\r\n leg_positions = [\r\n (-width/2 + leg_inset, -depth/2 + leg_inset),\r\n (width/2 - leg_inset, -depth/2 + leg_inset),\r\n (-width/2 + leg_inset, depth/2 - leg_inset),\r\n (width/2 - leg_inset, depth/2 - leg_inset)\r\n ]\r\n \r\n legs = []\r\n for i, (lx, ly) in enumerate(leg_positions):\r\n if leg_style == 'ROUND':\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=leg_size,\r\n depth=leg_height,\r\n location=(location[0] + lx, location[1] + ly, location[2] + leg_height/2)\r\n )\r\n elif leg_style == 'TAPERED':\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=leg_size * 1.5,\r\n radius2=leg_size * 0.8,\r\n depth=leg_height,\r\n location=(location[0] + lx, location[1] + ly, location[2] + leg_height/2)\r\n )\r\n else: # SQUARE\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + lx, location[1] + ly, location[2] + leg_height/2\r\n ))\r\n bpy.context.active_object.scale = (leg_size, leg_size, leg_height/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n leg = bpy.context.active_object\r\n leg.name = f\"{name}_Leg_{i+1}\"\r\n legs.append(leg)\r\n \r\n result['legs'] = legs\r\n \r\n # === MATERIAL ===\r\n mat = bpy.data.materials.new(f\"{name}_WoodMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.35, 0.22, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n \r\n top.data.materials.append(mat)\r\n for leg in legs:\r\n leg.data.materials.append(mat)\r\n \r\n result['material'] = mat\r\n \r\n return result\r\n\r\n\r\ndef create_desk_with_drawers(\r\n width: float = 1.4,\r\n depth: float = 0.7,\r\n height: float = 0.75,\r\n drawer_count: int = 2,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Desk\"\r\n) -> dict:\r\n \"\"\"Create a desk with drawer unit.\"\"\"\r\n result = create_table(\r\n table_type='DESK',\r\n width=width,\r\n depth=depth,\r\n height=height,\r\n location=location,\r\n name=name\r\n )\r\n \r\n # Add drawer unit on one side\r\n drawer_width = width * 0.35\r\n drawer_height = (height - 0.1) / drawer_count\r\n \r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] - width/2 + drawer_width/2 + 0.05,\r\n location[1],\r\n location[2] + height/2 - 0.02\r\n ))\r\n drawer_unit = bpy.context.active_object\r\n drawer_unit.name = f\"{name}_DrawerUnit\"\r\n drawer_unit.scale = (drawer_width/2, depth/2 - 0.03, height/2 - 0.05)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n result['drawer_unit'] = drawer_unit\r\n \r\n return result\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_table('DINING', location=(0, 0, 0))\r\n create_table('COFFEE', location=(3, 0, 0))\r\n create_table('ROUND', leg_style='ROUND', location=(-3, 0, 0))\r\n \r\n print(\"Created 3 table variations\")","category":"tasks/modeling","source_file":"tasks\\modeling\\table_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Torch Generator. Generates medieval-style torches with fire effects.","output":"import bpy\r\nimport math\r\nimport random\r\n\r\n\r\ndef create_torch(\r\n handle_length: float = 0.5,\r\n handle_radius: float = 0.025,\r\n head_size: float = 0.08,\r\n style: str = 'MEDIEVAL',\r\n lit: bool = True,\r\n wall_mounted: bool = False,\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Torch\"\r\n) -> dict:\r\n \"\"\"\r\n Create a torch.\r\n \r\n Args:\r\n handle_length: Handle length\r\n handle_radius: Handle thickness\r\n head_size: Burnable head size\r\n style: 'MEDIEVAL', 'PRIMITIVE'\r\n lit: Add fire effect\r\n wall_mounted: Angle for wall mount\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with torch parts\r\n \"\"\"\r\n result = {}\r\n \r\n # Handle\r\n bpy.ops.mesh.primitive_cylinder_add(\r\n radius=handle_radius,\r\n depth=handle_length,\r\n location=(location[0], location[1], location[2] + handle_length/2)\r\n )\r\n handle = bpy.context.active_object\r\n handle.name = f\"{name}_Handle\"\r\n \r\n # Handle material\r\n handle_mat = bpy.data.materials.new(f\"{name}_HandleMat\")\r\n bsdf = handle_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.25, 0.18, 0.1, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n handle.data.materials.append(handle_mat)\r\n \r\n result['handle'] = handle\r\n \r\n # Torch head\r\n head_z = location[2] + handle_length\r\n \r\n bpy.ops.mesh.primitive_ico_sphere_add(\r\n radius=head_size,\r\n subdivisions=2,\r\n location=(location[0], location[1], head_z)\r\n )\r\n head = bpy.context.active_object\r\n head.name = f\"{name}_Head\"\r\n head.scale.z = 1.3\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Deform for organic look\r\n for v in head.data.vertices:\r\n v.co.x += random.uniform(-0.01, 0.01)\r\n v.co.y += random.uniform(-0.01, 0.01)\r\n \r\n # Head material (wrapped cloth/oil)\r\n head_mat = bpy.data.materials.new(f\"{name}_HeadMat\")\r\n bsdf = head_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.15, 0.1, 0.05, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n head.data.materials.append(head_mat)\r\n \r\n result['head'] = head\r\n \r\n # Fire effect\r\n if lit:\r\n fire = _create_torch_fire(\r\n (location[0], location[1], head_z + head_size),\r\n head_size,\r\n name\r\n )\r\n result.update(fire)\r\n \r\n # Wall mount bracket\r\n if wall_mounted:\r\n # Rotate torch\r\n handle.rotation_euler.y = math.radians(-30)\r\n head.rotation_euler.y = math.radians(-30)\r\n \r\n # Add bracket\r\n bracket = _create_wall_bracket(location, name)\r\n result['bracket'] = bracket\r\n \r\n return result\r\n\r\n\r\ndef _create_torch_fire(\r\n location: tuple,\r\n size: float,\r\n name: str\r\n) -> dict:\r\n \"\"\"Create fire effect for torch.\"\"\"\r\n result = {}\r\n \r\n # Main flame\r\n bpy.ops.mesh.primitive_cone_add(\r\n radius1=size * 0.8,\r\n radius2=0,\r\n depth=size * 2,\r\n location=(location[0], location[1], location[2] + size)\r\n )\r\n flame = bpy.context.active_object\r\n flame.name = f\"{name}_Flame\"\r\n bpy.ops.object.shade_smooth()\r\n \r\n # Deform for natural flame shape\r\n for v in flame.data.vertices:\r\n if v.co.z > 0:\r\n offset = math.sin(v.co.z * 20) * 0.01\r\n v.co.x += offset\r\n v.co.y += offset\r\n \r\n # Flame material\r\n flame_mat = bpy.data.materials.new(f\"{name}_FlameMat\")\r\n flame_mat.blend_method = 'BLEND'\r\n bsdf = flame_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (1.0, 0.4, 0.0, 1.0)\r\n bsdf.inputs['Emission Color'].default_value = (1.0, 0.3, 0.0, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = 30.0\r\n bsdf.inputs['Alpha'].default_value = 0.8\r\n flame.data.materials.append(flame_mat)\r\n \r\n result['flame'] = flame\r\n \r\n # Point light\r\n bpy.ops.object.light_add(\r\n type='POINT',\r\n location=(location[0], location[1], location[2] + size * 0.5)\r\n )\r\n light = bpy.context.active_object\r\n light.name = f\"{name}_Light\"\r\n light.data.energy = 50\r\n light.data.color = (1.0, 0.6, 0.2)\r\n light.data.shadow_soft_size = size * 2\r\n \r\n result['light'] = light\r\n \r\n return result\r\n\r\n\r\ndef _create_wall_bracket(location: tuple, name: str) -> bpy.types.Object:\r\n \"\"\"Create wall mounting bracket.\"\"\"\r\n bpy.ops.mesh.primitive_cube_add(\r\n size=0.08,\r\n location=(location[0] - 0.1, location[1], location[2] + 0.25)\r\n )\r\n bracket = bpy.context.active_object\r\n bracket.name = f\"{name}_Bracket\"\r\n bracket.scale = (0.3, 0.5, 1)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n mat = bpy.data.materials.new(f\"{name}_BracketMat\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.3, 0.25, 0.2, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.8\r\n bsdf.inputs['Roughness'].default_value = 0.5\r\n bracket.data.materials.append(mat)\r\n \r\n return bracket\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_torch(lit=True, location=(0, 0, 0))\r\n create_torch(lit=True, wall_mounted=True, location=(0.5, 0, 0))\r\n \r\n print(\"Created torches\")","category":"tasks/modeling","source_file":"tasks\\modeling\\torch_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Window Generator. Generates various window types with frames and glass panes.","output":"import bpy\r\n\r\n\r\ndef create_window(\r\n width: float = 1.0,\r\n height: float = 1.2,\r\n frame_depth: float = 0.1,\r\n panes_x: int = 2,\r\n panes_y: int = 2,\r\n style: str = 'STANDARD',\r\n location: tuple = (0, 0, 0),\r\n name: str = \"Window\"\r\n) -> dict:\r\n \"\"\"\r\n Create a window with frame and glass.\r\n \r\n Args:\r\n width: Window width\r\n height: Window height\r\n frame_depth: Frame thickness\r\n panes_x: Horizontal pane divisions\r\n panes_y: Vertical pane divisions\r\n style: 'STANDARD', 'ARCHED', 'CIRCULAR'\r\n location: Position\r\n name: Object name\r\n \r\n Returns:\r\n Dictionary with window parts\r\n \"\"\"\r\n result = {}\r\n frame_width = 0.05\r\n \r\n # === OUTER FRAME ===\r\n frame_parts = _create_window_frame(\r\n width, height, frame_width, frame_depth, \r\n location, name, style\r\n )\r\n result['frame'] = frame_parts\r\n \r\n # === GLASS PANES ===\r\n inner_w = width - frame_width * 2\r\n inner_h = height - frame_width * 2\r\n pane_w = inner_w / panes_x\r\n pane_h = inner_h / panes_y\r\n \r\n glass_mat = bpy.data.materials.new(f\"{name}_GlassMat\")\r\n glass_mat.blend_method = 'BLEND'\r\n bsdf = glass_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.9, 1.0, 1.0)\r\n bsdf.inputs['Transmission Weight'].default_value = 0.95\r\n bsdf.inputs['Roughness'].default_value = 0.02\r\n bsdf.inputs['IOR'].default_value = 1.5\r\n \r\n panes = []\r\n divider_w = 0.02\r\n \r\n for px in range(panes_x):\r\n for py in range(panes_y):\r\n pane_center_x = location[0] - inner_w/2 + pane_w/2 + px * pane_w\r\n pane_center_z = location[2] - inner_h/2 + pane_h/2 + py * pane_h\r\n \r\n actual_w = pane_w - divider_w if panes_x > 1 else pane_w\r\n actual_h = pane_h - divider_w if panes_y > 1 else pane_h\r\n \r\n bpy.ops.mesh.primitive_plane_add(size=1, location=(\r\n pane_center_x,\r\n location[1],\r\n pane_center_z\r\n ))\r\n pane = bpy.context.active_object\r\n pane.name = f\"{name}_Glass_{px}_{py}\"\r\n pane.scale = (actual_w/2, 1, actual_h/2)\r\n pane.rotation_euler.x = 1.5708\r\n bpy.ops.object.transform_apply(scale=True, rotation=True)\r\n pane.data.materials.append(glass_mat)\r\n panes.append(pane)\r\n \r\n result['glass'] = panes\r\n \r\n # === DIVIDERS ===\r\n if panes_x > 1 or panes_y > 1:\r\n dividers = _create_dividers(\r\n width, height, frame_width, panes_x, panes_y,\r\n location, name\r\n )\r\n result['dividers'] = dividers\r\n \r\n return result\r\n\r\n\r\ndef _create_window_frame(\r\n width: float,\r\n height: float,\r\n frame_width: float,\r\n frame_depth: float,\r\n location: tuple,\r\n name: str,\r\n style: str\r\n) -> list:\r\n \"\"\"Create window frame.\"\"\"\r\n parts = []\r\n \r\n frame_mat = bpy.data.materials.new(f\"{name}_FrameMat\")\r\n bsdf = frame_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (0.95, 0.95, 0.93, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n \r\n # Top\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] + height/2 - frame_width/2\r\n ))\r\n top = bpy.context.active_object\r\n top.name = f\"{name}_Frame_Top\"\r\n top.scale = (width/2, frame_depth/2, frame_width/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n top.data.materials.append(frame_mat)\r\n parts.append(top)\r\n \r\n # Bottom\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] - height/2 + frame_width/2\r\n ))\r\n bottom = bpy.context.active_object\r\n bottom.name = f\"{name}_Frame_Bottom\"\r\n bottom.scale = (width/2, frame_depth/2, frame_width/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n bottom.data.materials.append(frame_mat)\r\n parts.append(bottom)\r\n \r\n # Sides\r\n inner_h = height - frame_width * 2\r\n for side, offset in [('L', -1), ('R', 1)]:\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] + offset * (width/2 - frame_width/2),\r\n location[1],\r\n location[2]\r\n ))\r\n side_frame = bpy.context.active_object\r\n side_frame.name = f\"{name}_Frame_{side}\"\r\n side_frame.scale = (frame_width/2, frame_depth/2, inner_h/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n side_frame.data.materials.append(frame_mat)\r\n parts.append(side_frame)\r\n \r\n return parts\r\n\r\n\r\ndef _create_dividers(\r\n width: float,\r\n height: float,\r\n frame_width: float,\r\n panes_x: int,\r\n panes_y: int,\r\n location: tuple,\r\n name: str\r\n) -> list:\r\n \"\"\"Create window dividers.\"\"\"\r\n dividers = []\r\n inner_w = width - frame_width * 2\r\n inner_h = height - frame_width * 2\r\n divider_size = 0.015\r\n \r\n divider_mat = bpy.data.materials.get(f\"{name}_FrameMat\")\r\n \r\n # Vertical dividers\r\n if panes_x > 1:\r\n pane_w = inner_w / panes_x\r\n for i in range(1, panes_x):\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0] - inner_w/2 + i * pane_w,\r\n location[1],\r\n location[2]\r\n ))\r\n div = bpy.context.active_object\r\n div.name = f\"{name}_DivV_{i}\"\r\n div.scale = (divider_size, divider_size, inner_h/2)\r\n bpy.ops.object.transform_apply(scale=True)\r\n if divider_mat:\r\n div.data.materials.append(divider_mat)\r\n dividers.append(div)\r\n \r\n # Horizontal dividers\r\n if panes_y > 1:\r\n pane_h = inner_h / panes_y\r\n for i in range(1, panes_y):\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(\r\n location[0],\r\n location[1],\r\n location[2] - inner_h/2 + i * pane_h\r\n ))\r\n div = bpy.context.active_object\r\n div.name = f\"{name}_DivH_{i}\"\r\n div.scale = (inner_w/2, divider_size, divider_size)\r\n bpy.ops.object.transform_apply(scale=True)\r\n if divider_mat:\r\n div.data.materials.append(divider_mat)\r\n dividers.append(div)\r\n \r\n return dividers\r\n\r\n\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n create_window(panes_x=1, panes_y=1, location=(0, 0, 1))\r\n create_window(panes_x=2, panes_y=3, location=(1.5, 0, 1))\r\n \r\n print(\"Created 2 window variations\")","category":"tasks/modeling","source_file":"tasks\\modeling\\window_generator.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Cycles GPU Rendering Setup. Configures Blender for optimal GPU rendering with Cycles, including device selection and quality settings.","output":"import bpy\r\n\r\n\r\ndef setup_cycles_gpu(\r\n device_type: str = 'OPTIX',\r\n samples: int = 256,\r\n use_denoising: bool = True,\r\n denoiser: str = 'OPTIX',\r\n use_adaptive_sampling: bool = True,\r\n noise_threshold: float = 0.01\r\n) -> dict:\r\n \"\"\"\r\n Configure Cycles for optimal GPU rendering.\r\n \r\n Args:\r\n device_type: 'OPTIX' (NVIDIA RTX), 'CUDA' (NVIDIA), 'HIP' (AMD), 'METAL' (Apple)\r\n samples: Render samples (higher = quality, lower = speed)\r\n use_denoising: Enable AI denoising\r\n denoiser: 'OPTIX', 'OPENIMAGEDENOISE', or 'NONE'\r\n use_adaptive_sampling: Stop early when noise is low\r\n noise_threshold: Adaptive sampling threshold (lower = higher quality)\r\n \r\n Returns:\r\n Dictionary with current settings\r\n \r\n Example:\r\n >>> setup_cycles_gpu('OPTIX', samples=512, denoiser='OPTIX')\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n # Set render engine to Cycles\r\n scene.render.engine = 'CYCLES'\r\n \r\n # Configure compute device\r\n preferences = bpy.context.preferences\r\n cycles_prefs = preferences.addons['cycles'].preferences\r\n \r\n # Set device type\r\n cycles_prefs.compute_device_type = device_type\r\n \r\n # Get and enable all devices of this type\r\n cycles_prefs.get_devices()\r\n for device in cycles_prefs.devices:\r\n device.use = True\r\n \r\n # Set scene to use GPU\r\n scene.cycles.device = 'GPU'\r\n \r\n # Sampling settings\r\n scene.cycles.samples = samples\r\n scene.cycles.use_adaptive_sampling = use_adaptive_sampling\r\n if use_adaptive_sampling:\r\n scene.cycles.adaptive_threshold = noise_threshold\r\n \r\n # Denoising\r\n scene.cycles.use_denoising = use_denoising\r\n if use_denoising:\r\n scene.cycles.denoiser = denoiser\r\n \r\n return {\r\n 'engine': 'CYCLES',\r\n 'device': 'GPU',\r\n 'device_type': device_type,\r\n 'samples': samples,\r\n 'denoising': use_denoising,\r\n 'adaptive_sampling': use_adaptive_sampling\r\n }\r\n\r\n\r\ndef setup_cycles_quality(\r\n preset: str = 'HIGH',\r\n custom_samples: int = None\r\n) -> dict:\r\n \"\"\"\r\n Apply quality presets for Cycles rendering.\r\n \r\n Args:\r\n preset: 'PREVIEW', 'MEDIUM', 'HIGH', 'ULTRA', or 'CUSTOM'\r\n custom_samples: Sample count for CUSTOM preset\r\n \r\n Returns:\r\n Applied settings dictionary\r\n \r\n Example:\r\n >>> setup_cycles_quality('HIGH')\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.render.engine = 'CYCLES'\r\n \r\n presets = {\r\n 'PREVIEW': {\r\n 'samples': 64,\r\n 'use_denoising': True,\r\n 'max_bounces': 4,\r\n 'diffuse_bounces': 2,\r\n 'glossy_bounces': 2,\r\n 'transmission_bounces': 4,\r\n },\r\n 'MEDIUM': {\r\n 'samples': 128,\r\n 'use_denoising': True,\r\n 'max_bounces': 8,\r\n 'diffuse_bounces': 4,\r\n 'glossy_bounces': 4,\r\n 'transmission_bounces': 8,\r\n },\r\n 'HIGH': {\r\n 'samples': 512,\r\n 'use_denoising': True,\r\n 'max_bounces': 12,\r\n 'diffuse_bounces': 6,\r\n 'glossy_bounces': 6,\r\n 'transmission_bounces': 12,\r\n },\r\n 'ULTRA': {\r\n 'samples': 2048,\r\n 'use_denoising': False,\r\n 'max_bounces': 16,\r\n 'diffuse_bounces': 8,\r\n 'glossy_bounces': 8,\r\n 'transmission_bounces': 16,\r\n },\r\n 'CUSTOM': {\r\n 'samples': custom_samples or 256,\r\n 'use_denoising': True,\r\n 'max_bounces': 12,\r\n 'diffuse_bounces': 6,\r\n 'glossy_bounces': 6,\r\n 'transmission_bounces': 12,\r\n }\r\n }\r\n \r\n settings = presets.get(preset.upper(), presets['HIGH'])\r\n \r\n scene.cycles.samples = settings['samples']\r\n scene.cycles.use_denoising = settings['use_denoising']\r\n scene.cycles.max_bounces = settings['max_bounces']\r\n scene.cycles.diffuse_bounces = settings['diffuse_bounces']\r\n scene.cycles.glossy_bounces = settings['glossy_bounces']\r\n scene.cycles.transmission_bounces = settings['transmission_bounces']\r\n \r\n return settings\r\n\r\n\r\ndef setup_transparent_render(\r\n enable: bool = True,\r\n format: str = 'PNG'\r\n) -> None:\r\n \"\"\"\r\n Configure render for transparent background.\r\n \r\n Args:\r\n enable: Enable transparent background\r\n format: Output format (PNG recommended for transparency)\r\n \r\n Example:\r\n >>> setup_transparent_render(True, 'PNG')\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n scene.render.film_transparent = enable\r\n scene.render.image_settings.file_format = format\r\n \r\n if format == 'PNG':\r\n scene.render.image_settings.color_mode = 'RGBA' if enable else 'RGB'\r\n scene.render.image_settings.color_depth = '16'\r\n\r\n\r\ndef setup_render_output(\r\n resolution: tuple = (1920, 1080),\r\n percentage: int = 100,\r\n output_path: str = \"//render_\",\r\n format: str = 'PNG',\r\n frame_range: tuple = None\r\n) -> None:\r\n \"\"\"\r\n Configure render output settings.\r\n \r\n Args:\r\n resolution: (width, height) in pixels\r\n percentage: Scale percentage (50 = half resolution)\r\n output_path: Output file path (// = relative)\r\n format: 'PNG', 'JPEG', 'EXR', 'FFMPEG'\r\n frame_range: (start, end) frames for animation\r\n \r\n Example:\r\n >>> setup_render_output((3840, 2160), 100, \"//4k_render_\", 'PNG')\r\n \"\"\"\r\n scene = bpy.context.scene\r\n \r\n scene.render.resolution_x = resolution[0]\r\n scene.render.resolution_y = resolution[1]\r\n scene.render.resolution_percentage = percentage\r\n scene.render.filepath = output_path\r\n scene.render.image_settings.file_format = format\r\n \r\n if frame_range:\r\n scene.frame_start = frame_range[0]\r\n scene.frame_end = frame_range[1]\r\n\r\n\r\ndef get_render_time_estimate(samples: int, resolution: tuple, scene_complexity: str = 'MEDIUM') -> str:\r\n \"\"\"\r\n Estimate render time based on settings (rough estimate).\r\n \r\n Args:\r\n samples: Render samples\r\n resolution: (width, height)\r\n scene_complexity: 'LOW', 'MEDIUM', 'HIGH'\r\n \r\n Returns:\r\n Estimated time string\r\n \"\"\"\r\n base_time = 0.001 # ms per pixel per sample\r\n \r\n complexity_multipliers = {\r\n 'LOW': 0.5,\r\n 'MEDIUM': 1.0,\r\n 'HIGH': 2.5\r\n }\r\n \r\n pixels = resolution[0] * resolution[1]\r\n multiplier = complexity_multipliers.get(scene_complexity.upper(), 1.0)\r\n \r\n total_ms = pixels * samples * base_time * multiplier\r\n total_seconds = total_ms / 1000\r\n \r\n if total_seconds < 60:\r\n return f\"~{int(total_seconds)} seconds\"\r\n elif total_seconds < 3600:\r\n return f\"~{int(total_seconds / 60)} minutes\"\r\n else:\r\n return f\"~{total_seconds / 3600:.1f} hours\"\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n # Configure for high-quality GPU rendering\r\n settings = setup_cycles_gpu('OPTIX', samples=512)\r\n setup_cycles_quality('HIGH')\r\n setup_render_output((1920, 1080), 100, \"//render_\")\r\n \r\n print(f\"Cycles GPU configured: {settings}\")\r\n print(f\"Estimated render time: {get_render_time_estimate(512, (1920, 1080), 'MEDIUM')}\")","category":"tasks/rendering","source_file":"tasks\\rendering\\cycles_gpu_setup.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Get Render Time Estimate. Category: tasks/rendering","output":"import bpy\n\ndef get_render_time_estimate(samples: int, resolution: tuple, scene_complexity: str = 'MEDIUM') -> str:\r\n \"\"\"\r\n Estimate render time based on settings (rough estimate).\r\n \r\n Args:\r\n samples: Render samples\r\n resolution: (width, height)\r\n scene_complexity: 'LOW', 'MEDIUM', 'HIGH'\r\n \r\n Returns:\r\n Estimated time string\r\n \"\"\"\r\n base_time = 0.001 # ms per pixel per sample\r\n \r\n complexity_multipliers = {\r\n 'LOW': 0.5,\r\n 'MEDIUM': 1.0,\r\n 'HIGH': 2.5\r\n }\r\n \r\n pixels = resolution[0] * resolution[1]\r\n multiplier = complexity_multipliers.get(scene_complexity.upper(), 1.0)\r\n \r\n total_ms = pixels * samples * base_time * multiplier\r\n total_seconds = total_ms / 1000\r\n \r\n if total_seconds < 60:\r\n return f\"~{int(total_seconds)} seconds\"\r\n elif total_seconds < 3600:\r\n return f\"~{int(total_seconds / 60)} minutes\"\r\n else:\r\n return f\"~{total_seconds / 3600:.1f} hours\"\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n # Configure for high-quality GPU rendering\r\n settings = setup_cycles_gpu('OPTIX', samples=512)\r\n setup_cycles_quality('HIGH')\r\n setup_render_output((1920, 1080), 100, \"//render_\")\r\n \r\n print(f\"Cycles GPU configured: {settings}\")\r\n print(f\"Estimated render time: {get_render_time_estimate(512, (1920, 1080), 'MEDIUM')}\")","category":"tasks/rendering","source_file":"tasks\\rendering\\cycles_gpu_setup.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Eevee Optimized Setup. Optimized Eevee render settings for fast previews and final renders.","output":"import bpy\r\n\r\n\r\ndef setup_eevee_quality(quality: str = 'MEDIUM') -> dict:\r\n \"\"\"\r\n Configure Eevee quality preset.\r\n \r\n Args:\r\n quality: 'LOW', 'MEDIUM', 'HIGH', 'ULTRA'\r\n \r\n Returns:\r\n Dictionary with applied settings\r\n \"\"\"\r\n bpy.context.scene.render.engine = 'BLENDER_EEVEE'\r\n eevee = bpy.context.scene.eevee\r\n \r\n presets = {\r\n 'LOW': {\r\n 'samples': 16,\r\n 'shadows': '512',\r\n 'ssr': False,\r\n 'ao': True,\r\n 'bloom': False\r\n },\r\n 'MEDIUM': {\r\n 'samples': 32,\r\n 'shadows': '1024',\r\n 'ssr': True,\r\n 'ao': True,\r\n 'bloom': True\r\n },\r\n 'HIGH': {\r\n 'samples': 64,\r\n 'shadows': '2048',\r\n 'ssr': True,\r\n 'ao': True,\r\n 'bloom': True\r\n },\r\n 'ULTRA': {\r\n 'samples': 128,\r\n 'shadows': '4096',\r\n 'ssr': True,\r\n 'ao': True,\r\n 'bloom': True\r\n }\r\n }\r\n \r\n preset = presets.get(quality, presets['MEDIUM'])\r\n \r\n eevee.taa_render_samples = preset['samples']\r\n eevee.shadow_cascade_size = preset['shadows']\r\n eevee.use_ssr = preset['ssr']\r\n eevee.use_gtao = preset['ao']\r\n eevee.use_bloom = preset['bloom']\r\n \r\n return preset\r\n\r\n\r\ndef enable_eevee_reflections(\r\n quality: str = 'MEDIUM',\r\n thickness: float = 0.5\r\n) -> None:\r\n \"\"\"Enable screen-space reflections.\"\"\"\r\n eevee = bpy.context.scene.eevee\r\n eevee.use_ssr = True\r\n eevee.use_ssr_refraction = True\r\n eevee.ssr_thickness = thickness\r\n \r\n if quality == 'HIGH':\r\n eevee.ssr_quality = 1.0\r\n eevee.ssr_max_roughness = 0.5\r\n else:\r\n eevee.ssr_quality = 0.5\r\n eevee.ssr_max_roughness = 0.3\r\n\r\n\r\ndef enable_eevee_shadows(soft: bool = True, contact: bool = True) -> None:\r\n \"\"\"Configure shadow settings.\"\"\"\r\n eevee = bpy.context.scene.eevee\r\n eevee.shadow_soft_max = 50 if soft else 0\r\n eevee.use_shadow_contact = contact\r\n\r\n\r\ndef enable_eevee_ambient_occlusion(\r\n distance: float = 0.5,\r\n factor: float = 1.0\r\n) -> None:\r\n \"\"\"Enable ambient occlusion.\"\"\"\r\n eevee = bpy.context.scene.eevee\r\n eevee.use_gtao = True\r\n eevee.gtao_distance = distance\r\n eevee.gtao_factor = factor\r\n\r\n\r\ndef enable_eevee_bloom(\r\n threshold: float = 0.8,\r\n intensity: float = 0.05,\r\n radius: float = 6.5\r\n) -> None:\r\n \"\"\"Enable bloom/glow effect.\"\"\"\r\n eevee = bpy.context.scene.eevee\r\n eevee.use_bloom = True\r\n eevee.bloom_threshold = threshold\r\n eevee.bloom_intensity = intensity\r\n eevee.bloom_radius = radius\r\n\r\n\r\ndef add_reflection_probe(\r\n location: tuple = (0, 0, 1),\r\n influence_distance: float = 2.5,\r\n probe_type: str = 'SPHERE',\r\n name: str = \"ReflectionProbe\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add reflection/light probe.\r\n \r\n Args:\r\n location: Probe position\r\n influence_distance: Influence radius\r\n probe_type: 'SPHERE' or 'BOX'\r\n name: Object name\r\n \r\n Returns:\r\n The probe object\r\n \"\"\"\r\n bpy.ops.object.lightprobe_add(\r\n type='SPHERE' if probe_type == 'SPHERE' else 'BOX',\r\n location=location\r\n )\r\n probe = bpy.context.active_object\r\n probe.name = name\r\n probe.data.influence_distance = influence_distance\r\n \r\n return probe\r\n\r\n\r\ndef add_irradiance_volume(\r\n location: tuple = (0, 0, 1),\r\n size: tuple = (5, 5, 3),\r\n resolution: tuple = (4, 4, 2),\r\n name: str = \"IrradianceVolume\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add irradiance volume for indirect lighting.\r\n \r\n Args:\r\n location: Volume center\r\n size: XYZ size\r\n resolution: XYZ probe count\r\n name: Object name\r\n \r\n Returns:\r\n The volume object\r\n \"\"\"\r\n bpy.ops.object.lightprobe_add(type='VOLUME', location=location)\r\n volume = bpy.context.active_object\r\n volume.name = name\r\n volume.scale = (size[0]/2, size[1]/2, size[2]/2)\r\n volume.data.grid_resolution_x = resolution[0]\r\n volume.data.grid_resolution_y = resolution[1]\r\n volume.data.grid_resolution_z = resolution[2]\r\n \r\n return volume\r\n\r\n\r\ndef bake_lighting() -> None:\r\n \"\"\"Bake indirect lighting for Eevee.\"\"\"\r\n bpy.ops.scene.light_cache_bake()\r\n\r\n\r\nif __name__ == \"__main__\":\r\n setup_eevee_quality('HIGH')\r\n enable_eevee_reflections()\r\n enable_eevee_bloom()\r\n \r\n print(\"Configured Eevee for high quality rendering\")","category":"tasks/rendering","source_file":"tasks\\rendering\\eevee_setup.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Setup Eevee Quality. Category: tasks/rendering","output":"import bpy\n\ndef setup_eevee_quality(quality: str = 'MEDIUM') -> dict:\r\n \"\"\"\r\n Configure Eevee quality preset.\r\n \r\n Args:\r\n quality: 'LOW', 'MEDIUM', 'HIGH', 'ULTRA'\r\n \r\n Returns:\r\n Dictionary with applied settings\r\n \"\"\"\r\n bpy.context.scene.render.engine = 'BLENDER_EEVEE'\r\n eevee = bpy.context.scene.eevee\r\n \r\n presets = {\r\n 'LOW': {\r\n 'samples': 16,\r\n 'shadows': '512',\r\n 'ssr': False,\r\n 'ao': True,\r\n 'bloom': False\r\n },\r\n 'MEDIUM': {\r\n 'samples': 32,\r\n 'shadows': '1024',\r\n 'ssr': True,\r\n 'ao': True,\r\n 'bloom': True\r\n },\r\n 'HIGH': {\r\n 'samples': 64,\r\n 'shadows': '2048',\r\n 'ssr': True,\r\n 'ao': True,\r\n 'bloom': True\r\n },\r\n 'ULTRA': {\r\n 'samples': 128,\r\n 'shadows': '4096',\r\n 'ssr': True,\r\n 'ao': True,\r\n 'bloom': True\r\n }\r\n }\r\n \r\n preset = presets.get(quality, presets['MEDIUM'])\r\n \r\n eevee.taa_render_samples = preset['samples']\r\n eevee.shadow_cascade_size = preset['shadows']\r\n eevee.use_ssr = preset['ssr']\r\n eevee.use_gtao = preset['ao']\r\n eevee.use_bloom = preset['bloom']\r\n \r\n return preset\r\n\r\n\r\ndef enable_eevee_reflections(\r\n quality: str = 'MEDIUM',\r\n thickness: float = 0.5\r\n) -> None:\r\n \"\"\"Enable screen-space reflections.\"\"\"\r\n eevee = bpy.context.scene.eevee\r\n eevee.use_ssr = True\r\n eevee.use_ssr_refraction = True\r\n eevee.ssr_thickness = thickness\r\n \r\n if quality == 'HIGH':\r\n eevee.ssr_quality = 1.0\r\n eevee.ssr_max_roughness = 0.5\r\n else:\r\n eevee.ssr_quality = 0.5\r\n eevee.ssr_max_roughness = 0.3\r\n\r\n\r\ndef enable_eevee_shadows(soft: bool = True, contact: bool = True) -> None:\r\n \"\"\"Configure shadow settings.\"\"\"\r\n eevee = bpy.context.scene.eevee\r\n eevee.shadow_soft_max = 50 if soft else 0\r\n eevee.use_shadow_contact = contact\r\n\r\n\r\ndef enable_eevee_ambient_occlusion(\r\n distance: float = 0.5,\r\n factor: float = 1.0\r\n) -> None:\r\n \"\"\"Enable ambient occlusion.\"\"\"\r\n eevee = bpy.context.scene.eevee\r\n eevee.use_gtao = True\r\n eevee.gtao_distance = distance\r\n eevee.gtao_factor = factor\r\n\r\n\r\ndef enable_eevee_bloom(\r\n threshold: float = 0.8,\r\n intensity: float = 0.05,\r\n radius: float = 6.5\r\n) -> None:\r\n \"\"\"Enable bloom/glow effect.\"\"\"\r\n eevee = bpy.context.scene.eevee\r\n eevee.use_bloom = True\r\n eevee.bloom_threshold = threshold\r\n eevee.bloom_intensity = intensity\r\n eevee.bloom_radius = radius\r\n\r\n\r\ndef add_reflection_probe(\r\n location: tuple = (0, 0, 1),\r\n influence_distance: float = 2.5,\r\n probe_type: str = 'SPHERE',\r\n name: str = \"ReflectionProbe\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add reflection/light probe.\r\n \r\n Args:\r\n location: Probe position\r\n influence_distance: Influence radius\r\n probe_type: 'SPHERE' or 'BOX'\r\n name: Object name\r\n \r\n Returns:\r\n The probe object\r\n \"\"\"\r\n bpy.ops.object.lightprobe_add(\r\n type='SPHERE' if probe_type == 'SPHERE' else 'BOX',\r\n location=location\r\n )\r\n probe = bpy.context.active_object\r\n probe.name = name\r\n probe.data.influence_distance = influence_distance\r\n \r\n return probe\r\n\r\n\r\ndef add_irradiance_volume(\r\n location: tuple = (0, 0, 1),\r\n size: tuple = (5, 5, 3),\r\n resolution: tuple = (4, 4, 2),\r\n name: str = \"IrradianceVolume\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add irradiance volume for indirect lighting.\r\n \r\n Args:\r\n location: Volume center\r\n size: XYZ size\r\n resolution: XYZ probe count\r\n name: Object name\r\n \r\n Returns:\r\n The volume object\r\n \"\"\"\r\n bpy.ops.object.lightprobe_add(type='VOLUME', location=location)\r\n volume = bpy.context.active_object\r\n volume.name = name\r\n volume.scale = (size[0]/2, size[1]/2, size[2]/2)\r\n volume.data.grid_resolution_x = resolution[0]\r\n volume.data.grid_resolution_y = resolution[1]\r\n volume.data.grid_resolution_z = resolution[2]\r\n \r\n return volume\r\n\r\n\r\ndef bake_lighting() -> None:\r\n \"\"\"Bake indirect lighting for Eevee.\"\"\"\r\n bpy.ops.scene.light_cache_bake()\r\n\r\n\r\nif __name__ == \"__main__\":\r\n setup_eevee_quality('HIGH')\r\n enable_eevee_reflections()\r\n enable_eevee_bloom()\r\n \r\n print(\"Configured Eevee for high quality rendering\")","category":"tasks/rendering","source_file":"tasks\\rendering\\eevee_setup.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Product Photography Shot Setup. Complete product photography scene setup with backdrop, lighting, camera, and render settings.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_product_shot_scene(\r\n product_height: float = 1.0,\r\n backdrop_color: tuple = (1.0, 1.0, 1.0),\r\n style: str = 'CLEAN',\r\n camera_angle: str = 'FRONT',\r\n resolution: tuple = (1920, 1920),\r\n name_prefix: str = \"ProductShot\"\r\n) -> dict:\r\n \"\"\"\r\n Create a complete product photography scene.\r\n \r\n Args:\r\n product_height: Height of product for proper camera framing\r\n backdrop_color: RGB backdrop color\r\n style: 'CLEAN' (white), 'DARK' (black), 'GRADIENT', 'STUDIO'\r\n camera_angle: 'FRONT', 'THREE_QUARTER', 'TOP_DOWN', 'LOW_ANGLE'\r\n resolution: Render resolution (width, height)\r\n name_prefix: Prefix for created objects\r\n \r\n Returns:\r\n Dictionary with all created objects\r\n \r\n Example:\r\n >>> scene = create_product_shot_scene(product_height=0.5, style='DARK')\r\n \"\"\"\r\n result = {}\r\n \r\n # === BACKDROP ===\r\n backdrop = _create_backdrop(backdrop_color, style, product_height, name_prefix)\r\n result['backdrop'] = backdrop\r\n \r\n # === LIGHTING ===\r\n lights = _create_product_lighting(product_height, style, name_prefix)\r\n result.update(lights)\r\n \r\n # === CAMERA ===\r\n camera = _create_product_camera(product_height, camera_angle, name_prefix)\r\n result['camera'] = camera\r\n bpy.context.scene.camera = camera\r\n \r\n # === RENDER SETTINGS ===\r\n _configure_render_settings(resolution, style)\r\n \r\n # === WORLD ===\r\n _setup_world(backdrop_color, style)\r\n \r\n return result\r\n\r\n\r\ndef _create_backdrop(\r\n color: tuple,\r\n style: str,\r\n product_height: float,\r\n name_prefix: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create curved backdrop.\"\"\"\r\n scale = product_height * 3\r\n \r\n # Create base plane\r\n bpy.ops.mesh.primitive_plane_add(size=scale * 2, location=(0, scale * 0.8, 0))\r\n backdrop = bpy.context.active_object\r\n backdrop.name = f\"{name_prefix}_Backdrop\"\r\n \r\n # Rotate to vertical\r\n backdrop.rotation_euler[0] = math.radians(90)\r\n \r\n # Add bend modifier for curved sweep\r\n bpy.ops.object.modifier_add(type='SIMPLE_DEFORM')\r\n backdrop.modifiers[\"SimpleDeform\"].deform_method = 'BEND'\r\n backdrop.modifiers[\"SimpleDeform\"].angle = math.radians(-60)\r\n backdrop.modifiers[\"SimpleDeform\"].deform_axis = 'X'\r\n \r\n # Subdivide for smooth curve\r\n bpy.ops.object.modifier_add(type='SUBSURF')\r\n backdrop.modifiers[\"Subdivision\"].levels = 3\r\n backdrop.modifiers[\"Subdivision\"].render_levels = 3\r\n \r\n # Material\r\n mat = bpy.data.materials.new(f\"{name_prefix}_BackdropMat\")\r\n nodes = mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n if style == 'CLEAN':\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.9\r\n elif style == 'DARK':\r\n bsdf.inputs['Base Color'].default_value = (0.02, 0.02, 0.02, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.8\r\n elif style == 'GRADIENT':\r\n # Create gradient with color ramp\r\n pass\r\n elif style == 'STUDIO':\r\n bsdf.inputs['Base Color'].default_value = (0.8, 0.8, 0.8, 1.0)\r\n bsdf.inputs['Roughness'].default_value = 0.6\r\n bsdf.inputs['Specular IOR Level'].default_value = 0.2\r\n \r\n backdrop.data.materials.append(mat)\r\n \r\n return backdrop\r\n\r\n\r\ndef _create_product_lighting(\r\n product_height: float,\r\n style: str,\r\n name_prefix: str\r\n) -> dict:\r\n \"\"\"Create product photography lighting.\"\"\"\r\n lights = {}\r\n scale = product_height * 2\r\n \r\n # Key light\r\n bpy.ops.object.light_add(type='AREA', location=(scale * 1.5, -scale, scale * 1.5))\r\n key = bpy.context.active_object\r\n key.name = f\"{name_prefix}_Key\"\r\n key.data.shape = 'RECTANGLE'\r\n key.data.size = scale * 1.2\r\n key.data.size_y = scale * 0.8\r\n \r\n if style == 'CLEAN':\r\n key.data.energy = 800\r\n elif style == 'DARK':\r\n key.data.energy = 500\r\n else:\r\n key.data.energy = 600\r\n \r\n # Point at product center\r\n direction = (-scale * 1.5, scale, -scale * 0.5)\r\n key.rotation_euler = _direction_to_euler(direction)\r\n lights['key'] = key\r\n \r\n # Fill light\r\n bpy.ops.object.light_add(type='AREA', location=(-scale * 1.2, -scale * 0.8, scale))\r\n fill = bpy.context.active_object\r\n fill.name = f\"{name_prefix}_Fill\"\r\n fill.data.shape = 'RECTANGLE'\r\n fill.data.size = scale * 1.5\r\n fill.data.size_y = scale\r\n fill.data.energy = key.data.energy * 0.4\r\n \r\n direction = (scale * 1.2, scale * 0.8, -scale * 0.5)\r\n fill.rotation_euler = _direction_to_euler(direction)\r\n lights['fill'] = fill\r\n \r\n # Rim light (for separation)\r\n bpy.ops.object.light_add(type='AREA', location=(0, scale * 1.5, scale * 0.5))\r\n rim = bpy.context.active_object\r\n rim.name = f\"{name_prefix}_Rim\"\r\n rim.data.shape = 'RECTANGLE'\r\n rim.data.size = scale * 0.8\r\n rim.data.size_y = scale * 1.2\r\n rim.data.energy = key.data.energy * 0.3\r\n \r\n direction = (0, -scale * 1.5, 0)\r\n rim.rotation_euler = _direction_to_euler(direction)\r\n lights['rim'] = rim\r\n \r\n # Top bounce (for even illumination)\r\n if style == 'CLEAN':\r\n bpy.ops.object.light_add(type='AREA', location=(0, 0, scale * 2.5))\r\n top = bpy.context.active_object\r\n top.name = f\"{name_prefix}_Top\"\r\n top.data.shape = 'DISK'\r\n top.data.size = scale * 2\r\n top.data.energy = key.data.energy * 0.5\r\n top.rotation_euler = (math.radians(180), 0, 0)\r\n lights['top'] = top\r\n \r\n return lights\r\n\r\n\r\ndef _create_product_camera(\r\n product_height: float,\r\n angle: str,\r\n name_prefix: str\r\n) -> bpy.types.Object:\r\n \"\"\"Create product camera with proper framing.\"\"\"\r\n scale = product_height * 2\r\n \r\n angles = {\r\n 'FRONT': (scale * 2, 0, product_height * 0.6),\r\n 'THREE_QUARTER': (scale * 1.8, -scale * 1.5, product_height * 0.8),\r\n 'TOP_DOWN': (0, 0, scale * 3),\r\n 'LOW_ANGLE': (scale * 2.5, -scale * 0.5, product_height * 0.2)\r\n }\r\n \r\n position = angles.get(angle, angles['THREE_QUARTER'])\r\n \r\n bpy.ops.object.camera_add(location=position)\r\n camera = bpy.context.active_object\r\n camera.name = f\"{name_prefix}_Camera\"\r\n \r\n # Point at product center\r\n from mathutils import Vector\r\n target = Vector((0, 0, product_height / 2))\r\n direction = target - camera.location\r\n camera.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()\r\n \r\n # Camera settings\r\n camera.data.lens = 85 # Portrait lens\r\n camera.data.sensor_width = 36\r\n \r\n # DOF\r\n camera.data.dof.use_dof = True\r\n camera.data.dof.aperture_fstop = 4.0\r\n camera.data.dof.focus_distance = direction.length\r\n \r\n return camera\r\n\r\n\r\ndef _configure_render_settings(resolution: tuple, style: str) -> None:\r\n \"\"\"Configure render settings for product photography.\"\"\"\r\n scene = bpy.context.scene\r\n \r\n # Resolution\r\n scene.render.resolution_x = resolution[0]\r\n scene.render.resolution_y = resolution[1]\r\n scene.render.resolution_percentage = 100\r\n \r\n # Use Cycles for best quality\r\n scene.render.engine = 'CYCLES'\r\n scene.cycles.samples = 256\r\n scene.cycles.use_denoising = True\r\n \r\n # Transparent background\r\n scene.render.film_transparent = style != 'STUDIO'\r\n scene.render.image_settings.file_format = 'PNG'\r\n scene.render.image_settings.color_mode = 'RGBA'\r\n\r\n\r\ndef _setup_world(color: tuple, style: str) -> None:\r\n \"\"\"Set up world environment.\"\"\"\r\n world = bpy.context.scene.world\r\n if world is None:\r\n world = bpy.data.worlds.new(\"ProductWorld\")\r\n bpy.context.scene.world = world\r\n \r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n \r\n background = nodes.new('ShaderNodeBackground')\r\n \r\n if style == 'CLEAN':\r\n background.inputs['Color'].default_value = (*color, 1.0)\r\n background.inputs['Strength'].default_value = 0.5\r\n elif style == 'DARK':\r\n background.inputs['Color'].default_value = (0.01, 0.01, 0.01, 1.0)\r\n background.inputs['Strength'].default_value = 0.2\r\n else:\r\n background.inputs['Color'].default_value = (0.8, 0.8, 0.8, 1.0)\r\n background.inputs['Strength'].default_value = 0.3\r\n \r\n output = nodes.new('ShaderNodeOutputWorld')\r\n links.new(background.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef _direction_to_euler(direction: tuple) -> tuple:\r\n \"\"\"Convert direction to euler rotation.\"\"\"\r\n from mathutils import Vector\r\n vec = Vector(direction).normalized()\r\n rot_quat = vec.to_track_quat('-Z', 'Y')\r\n return rot_quat.to_euler()\r\n\r\n\r\n# Standalone execution\r\nif __name__ == \"__main__\":\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create test product\r\n bpy.ops.mesh.primitive_cube_add(size=0.5, location=(0, 0, 0.25))\r\n \r\n # Create product shot scene\r\n scene = create_product_shot_scene(\r\n product_height=0.5,\r\n style='CLEAN',\r\n camera_angle='THREE_QUARTER'\r\n )\r\n \r\n print(f\"Created product shot with {len(scene)} elements\")","category":"tasks/rendering","source_file":"tasks\\rendering\\product_shot.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: NPR/Toon Rendering Setup. Sets up non-photorealistic/toon rendering with outlines.","output":"import bpy\r\n\r\n\r\ndef setup_toon_shading(\r\n obj: bpy.types.Object,\r\n base_color: tuple = (1, 0.5, 0.3),\r\n shadow_color: tuple = (0.4, 0.2, 0.1),\r\n steps: int = 3,\r\n name: str = \"ToonMat\"\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Apply toon shading material to object.\r\n \r\n Args:\r\n obj: Target object\r\n base_color: RGB main color\r\n shadow_color: RGB shadow color\r\n steps: Cell shading steps\r\n name: Material name\r\n \r\n Returns:\r\n The created material\r\n \"\"\"\r\n mat = bpy.data.materials.new(name)\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Shader to RGB for Eevee toon effect\r\n diffuse = nodes.new('ShaderNodeBsdfDiffuse')\r\n diffuse.location = (-400, 0)\r\n diffuse.inputs['Color'].default_value = (*base_color, 1.0)\r\n \r\n shader_to_rgb = nodes.new('ShaderNodeShaderToRGB')\r\n shader_to_rgb.location = (-200, 0)\r\n \r\n # Color ramp for cel shading\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (0, 0)\r\n ramp.color_ramp.interpolation = 'CONSTANT'\r\n \r\n # Configure steps\r\n ramp.color_ramp.elements[0].position = 0\r\n ramp.color_ramp.elements[0].color = (*shadow_color, 1.0)\r\n \r\n if steps >= 2:\r\n ramp.color_ramp.elements[1].position = 0.5\r\n ramp.color_ramp.elements[1].color = (*base_color, 1.0)\r\n \r\n if steps >= 3:\r\n elem = ramp.color_ramp.elements.new(0.8)\r\n highlight = tuple(min(1, c * 1.3) for c in base_color)\r\n elem.color = (*highlight, 1.0)\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (200, 0)\r\n \r\n links.new(diffuse.outputs['BSDF'], shader_to_rgb.inputs['Shader'])\r\n links.new(shader_to_rgb.outputs['Color'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], output.inputs['Surface'])\r\n \r\n obj.data.materials.clear()\r\n obj.data.materials.append(mat)\r\n \r\n return mat\r\n\r\n\r\ndef enable_freestyle_outlines(\r\n line_thickness: float = 2.0,\r\n line_color: tuple = (0, 0, 0)\r\n) -> None:\r\n \"\"\"\r\n Enable Freestyle for outline rendering.\r\n \r\n Args:\r\n line_thickness: Line thickness in pixels\r\n line_color: RGB line color\r\n \"\"\"\r\n bpy.context.scene.render.use_freestyle = True\r\n \r\n view_layer = bpy.context.view_layer\r\n view_layer.freestyle_settings.as_render_pass = True\r\n \r\n # Configure line set\r\n if not view_layer.freestyle_settings.linesets:\r\n view_layer.freestyle_settings.linesets.new(\"OutlineSet\")\r\n \r\n lineset = view_layer.freestyle_settings.linesets[0]\r\n lineset.select_silhouette = True\r\n lineset.select_border = True\r\n lineset.select_crease = True\r\n lineset.select_edge_mark = False\r\n \r\n # Line style\r\n linestyle = lineset.linestyle\r\n linestyle.color = line_color\r\n linestyle.thickness = line_thickness\r\n\r\n\r\ndef setup_grease_pencil_outline(\r\n obj: bpy.types.Object,\r\n thickness: float = 50,\r\n color: tuple = (0, 0, 0),\r\n name: str = \"GPOutline\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Add Grease Pencil outline to object.\r\n \r\n Args:\r\n obj: Target object\r\n thickness: Outline thickness\r\n color: RGB outline color\r\n name: GP object name\r\n \r\n Returns:\r\n The Grease Pencil object\r\n \"\"\"\r\n # Create GP object\r\n gp_data = bpy.data.grease_pencils.new(name)\r\n gp_obj = bpy.data.objects.new(name, gp_data)\r\n bpy.context.collection.objects.link(gp_obj)\r\n \r\n # Add layer\r\n layer = gp_data.layers.new(\"Outline\")\r\n frame = layer.frames.new(1)\r\n \r\n # Line art modifier on GP\r\n mod = gp_obj.grease_pencil_modifiers.new(\"LineArt\", 'GP_LINEART')\r\n mod.source_type = 'OBJECT'\r\n mod.source_object = obj\r\n mod.thickness = int(thickness)\r\n \r\n # Create material for outline\r\n gp_mat = bpy.data.materials.new(f\"{name}_Mat\")\r\n bpy.data.materials.create_gpencil_data(gp_mat)\r\n gp_mat.grease_pencil.color = (*color, 1.0)\r\n gp_data.materials.append(gp_mat)\r\n \r\n return gp_obj\r\n\r\n\r\ndef setup_eevee_for_toon() -> None:\r\n \"\"\"Configure Eevee settings for toon rendering.\"\"\"\r\n bpy.context.scene.render.engine = 'BLENDER_EEVEE'\r\n \r\n eevee = bpy.context.scene.eevee\r\n eevee.taa_render_samples = 32\r\n \r\n # Disable reflections/refractions for flat look\r\n eevee.use_ssr = False\r\n \r\n # Simple shadows\r\n eevee.shadow_cascade_size = '1024'\r\n\r\n\r\ndef create_toon_scene(\r\n target: bpy.types.Object = None,\r\n outline_thickness: float = 2.0\r\n) -> None:\r\n \"\"\"\r\n Set up complete toon rendering scene.\r\n \r\n Args:\r\n target: Object to apply toon shading\r\n outline_thickness: Outline size\r\n \"\"\"\r\n setup_eevee_for_toon()\r\n enable_freestyle_outlines(line_thickness=outline_thickness)\r\n \r\n if target:\r\n setup_toon_shading(target)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n # Apply toon shading to active object\r\n obj = bpy.context.active_object\r\n if obj and obj.type == 'MESH':\r\n setup_toon_shading(obj, base_color=(0.9, 0.5, 0.2))\r\n enable_freestyle_outlines()\r\n print(\"Applied toon shading\")\r\n else:\r\n print(\"Select a mesh object first\")","category":"tasks/rendering","source_file":"tasks\\rendering\\toon_setup.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Setup Eevee For Toon. Category: tasks/rendering","output":"import bpy\n\ndef setup_eevee_for_toon() -> None:\r\n \"\"\"Configure Eevee settings for toon rendering.\"\"\"\r\n bpy.context.scene.render.engine = 'BLENDER_EEVEE'\r\n \r\n eevee = bpy.context.scene.eevee\r\n eevee.taa_render_samples = 32\r\n \r\n # Disable reflections/refractions for flat look\r\n eevee.use_ssr = False\r\n \r\n # Simple shadows\r\n eevee.shadow_cascade_size = '1024'\r\n\r\n\r\ndef create_toon_scene(\r\n target: bpy.types.Object = None,\r\n outline_thickness: float = 2.0\r\n) -> None:\r\n \"\"\"\r\n Set up complete toon rendering scene.\r\n \r\n Args:\r\n target: Object to apply toon shading\r\n outline_thickness: Outline size\r\n \"\"\"\r\n setup_eevee_for_toon()\r\n enable_freestyle_outlines(line_thickness=outline_thickness)\r\n \r\n if target:\r\n setup_toon_shading(target)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n # Apply toon shading to active object\r\n obj = bpy.context.active_object\r\n if obj and obj.type == 'MESH':\r\n setup_toon_shading(obj, base_color=(0.9, 0.5, 0.2))\r\n enable_freestyle_outlines()\r\n print(\"Applied toon shading\")\r\n else:\r\n print(\"Select a mesh object first\")","category":"tasks/rendering","source_file":"tasks\\rendering\\toon_setup.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Create Simple Car with Wheels. Creates a complete simple car model with body, four wheels, windows, and headlights. Ideal as a base for vehicle scenes.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef create_simple_car(\r\n location: tuple = (0, 0, 0),\r\n body_color: tuple = (0.8, 0.1, 0.1, 1.0),\r\n name_prefix: str = \"Car\"\r\n) -> dict:\r\n \"\"\"\r\n Create a complete simple car with body, wheels, windows, and headlights.\r\n \r\n Args:\r\n location: Base position for the car\r\n body_color: RGBA color for car body\r\n name_prefix: Prefix for all created objects\r\n \r\n Returns:\r\n Dictionary containing all created objects\r\n \r\n Example:\r\n >>> car = create_simple_car((0, 0, 0), (0.1, 0.2, 0.8, 1.0), \"SportsCar\")\r\n >>> print(car['body'].name) # \"SportsCar_Body\"\r\n \"\"\"\r\n created_objects = {}\r\n bx, by, bz = location\r\n \r\n # Deselect all\r\n bpy.ops.object.select_all(action='DESELECT')\r\n \r\n # === CAR BODY ===\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(bx, by, bz + 0.5))\r\n body = bpy.context.active_object\r\n body.name = f\"{name_prefix}_Body\"\r\n body.scale = (2.0, 0.9, 0.5)\r\n bpy.ops.object.transform_apply(scale=True)\r\n \r\n # Body material\r\n body_mat = bpy.data.materials.new(name=f\"{name_prefix}_Paint\")\r\n bsdf = body_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = body_color\r\n bsdf.inputs['Metallic'].default_value = 0.9\r\n bsdf.inputs['Roughness'].default_value = 0.2\r\n body.data.materials.append(body_mat)\r\n created_objects['body'] = body\r\n \r\n # === CABIN/ROOF ===\r\n bpy.ops.mesh.primitive_cube_add(size=1, location=(bx - 0.2, by, bz + 0.95))\r\n cabin = bpy.context.active_object\r\n cabin.name = f\"{name_prefix}_Cabin\"\r\n cabin.scale = (1.0, 0.85, 0.35)\r\n bpy.ops.object.transform_apply(scale=True)\r\n cabin.data.materials.append(body_mat)\r\n created_objects['cabin'] = cabin\r\n \r\n # === WHEELS ===\r\n wheel_positions = [\r\n (bx + 0.7, by + 0.55, bz + 0.25), # Front Right\r\n (bx + 0.7, by - 0.55, bz + 0.25), # Front Left\r\n (bx - 0.7, by + 0.55, bz + 0.25), # Rear Right\r\n (bx - 0.7, by - 0.55, bz + 0.25), # Rear Left\r\n ]\r\n wheel_names = [\"FR\", \"FL\", \"RR\", \"RL\"]\r\n \r\n # Wheel material\r\n wheel_mat = bpy.data.materials.new(name=f\"{name_prefix}_Rubber\")\r\n wheel_bsdf = wheel_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n wheel_bsdf.inputs['Base Color'].default_value = (0.02, 0.02, 0.02, 1.0)\r\n wheel_bsdf.inputs['Roughness'].default_value = 0.8\r\n \r\n wheels = []\r\n for pos, wname in zip(wheel_positions, wheel_names):\r\n bpy.ops.mesh.primitive_cylinder_add(radius=0.25, depth=0.15, location=pos)\r\n wheel = bpy.context.active_object\r\n wheel.name = f\"{name_prefix}_Wheel_{wname}\"\r\n wheel.rotation_euler[0] = math.radians(90)\r\n wheel.data.materials.append(wheel_mat)\r\n wheels.append(wheel)\r\n created_objects['wheels'] = wheels\r\n \r\n # === WINDOWS ===\r\n glass_mat = bpy.data.materials.new(name=f\"{name_prefix}_Glass\")\r\n glass_bsdf = glass_mat.node_tree.nodes.get(\"Principled BSDF\")\r\n glass_bsdf.inputs['Base Color'].default_value = (0.1, 0.1, 0.15, 1.0)\r\n glass_bsdf.inputs['Metallic'].default_value = 0.0\r\n glass_bsdf.inputs['Roughness'].default_value = 0.0\r\n glass_bsdf.inputs['Transmission'].default_value = 0.9\r\n \r\n # Front windshield\r\n bpy.ops.mesh.primitive_plane_add(size=0.8, location=(bx + 0.35, by, bz + 0.95))\r\n windshield = bpy.context.active_object\r\n windshield.name = f\"{name_prefix}_Windshield\"\r\n windshield.rotation_euler[1] = math.radians(70)\r\n windshield.scale[1] = 1.0\r\n windshield.data.materials.append(glass_mat)\r\n created_objects['windshield'] = windshield\r\n \r\n # Rear window\r\n bpy.ops.mesh.primitive_plane_add(size=0.7, location=(bx - 0.6, by, bz + 0.9))\r\n rear_window = bpy.context.active_object\r\n rear_window.name = f\"{name_prefix}_RearWindow\"\r\n rear_window.rotation_euler[1] = math.radians(-60)\r\n rear_window.data.materials.append(glass_mat)\r\n created_objects['rear_window'] = rear_window\r\n \r\n # === HEADLIGHTS ===\r\n headlight_mat = bpy.data.materials.new(name=f\"{name_prefix}_Headlight\")\r\n nodes = headlight_mat.node_tree.nodes\r\n links = headlight_mat.node_tree.links\r\n nodes.clear()\r\n emission = nodes.new('ShaderNodeEmission')\r\n emission.inputs['Color'].default_value = (1.0, 0.95, 0.8, 1.0)\r\n emission.inputs['Strength'].default_value = 5.0\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n links.new(emission.outputs['Emission'], output.inputs['Surface'])\r\n \r\n headlights = []\r\n for y_offset in [0.35, -0.35]:\r\n bpy.ops.mesh.primitive_uv_sphere_add(radius=0.08, location=(bx + 1.0, by + y_offset, bz + 0.45))\r\n hl = bpy.context.active_object\r\n hl.name = f\"{name_prefix}_Headlight_{'R' if y_offset > 0 else 'L'}\"\r\n hl.data.materials.append(headlight_mat)\r\n headlights.append(hl)\r\n created_objects['headlights'] = headlights\r\n \r\n # === TAILLIGHTS ===\r\n taillight_mat = bpy.data.materials.new(name=f\"{name_prefix}_Taillight\")\r\n nodes = taillight_mat.node_tree.nodes\r\n links = taillight_mat.node_tree.links\r\n nodes.clear()\r\n emission = nodes.new('ShaderNodeEmission')\r\n emission.inputs['Color'].default_value = (1.0, 0.0, 0.0, 1.0)\r\n emission.inputs['Strength'].default_value = 3.0\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n links.new(emission.outputs['Emission'], output.inputs['Surface'])\r\n \r\n taillights = []\r\n for y_offset in [0.35, -0.35]:\r\n bpy.ops.mesh.primitive_cube_add(size=0.1, location=(bx - 1.0, by + y_offset, bz + 0.5))\r\n tl = bpy.context.active_object\r\n tl.name = f\"{name_prefix}_Taillight_{'R' if y_offset > 0 else 'L'}\"\r\n tl.scale = (0.5, 1.5, 1.0)\r\n tl.data.materials.append(taillight_mat)\r\n taillights.append(tl)\r\n created_objects['taillights'] = taillights\r\n \r\n return created_objects\r\n\r\n\r\n# Standalone execution example\r\nif __name__ == \"__main__\":\r\n # Clear existing meshes\r\n bpy.ops.object.select_all(action='SELECT')\r\n bpy.ops.object.delete()\r\n \r\n # Create a red sports car\r\n car = create_simple_car(\r\n location=(0, 0, 0),\r\n body_color=(0.7, 0.05, 0.05, 1.0),\r\n name_prefix=\"SportsCar\"\r\n )\r\n \r\n print(f\"Created car with {len(car)} component groups\")","category":"tasks/vehicles","source_file":"tasks\\vehicles\\simple_car.py","pair_type":"full_script"} +{"instruction":"Write a complete Blender Python script: Texture Paint Utilities. Functions for texture painting and image editing.","output":"import bpy\r\n\r\n\r\ndef enable_texture_paint_mode(obj: bpy.types.Object = None) -> None:\r\n \"\"\"Enter texture paint mode.\"\"\"\r\n if obj:\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='TEXTURE_PAINT')\r\n\r\n\r\ndef create_paint_slot(\r\n obj: bpy.types.Object,\r\n slot_type: str = 'DIFFUSE',\r\n width: int = 1024,\r\n height: int = 1024,\r\n color: tuple = (0.5, 0.5, 0.5, 1.0),\r\n name: str = \"PaintTexture\"\r\n) -> bpy.types.Image:\r\n \"\"\"\r\n Create texture paint slot.\r\n \r\n Args:\r\n obj: Target object\r\n slot_type: 'DIFFUSE', 'ROUGHNESS', 'NORMAL', 'BUMP'\r\n width: Image width\r\n height: Image height\r\n color: Fill color\r\n name: Texture name\r\n \r\n Returns:\r\n Created image\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='TEXTURE_PAINT')\r\n \r\n bpy.ops.paint.add_texture_paint_slot(\r\n type=slot_type,\r\n width=width,\r\n height=height,\r\n color=color,\r\n name=name\r\n )\r\n \r\n return bpy.data.images.get(name)\r\n\r\n\r\ndef set_paint_brush(brush_name: str) -> None:\r\n \"\"\"\r\n Set active paint brush.\r\n \r\n Args:\r\n brush_name: 'TexDraw', 'Soften', 'Smear', 'Clone', 'Fill', 'Mask'\r\n \"\"\"\r\n brush = bpy.data.brushes.get(brush_name)\r\n if brush:\r\n bpy.context.tool_settings.image_paint.brush = brush\r\n\r\n\r\ndef set_brush_color(color: tuple = (1.0, 1.0, 1.0)) -> None:\r\n \"\"\"Set brush primary color.\"\"\"\r\n brush = bpy.context.tool_settings.image_paint.brush\r\n if brush:\r\n brush.color = color\r\n\r\n\r\ndef set_brush_secondary_color(color: tuple = (0.0, 0.0, 0.0)) -> None:\r\n \"\"\"Set brush secondary color.\"\"\"\r\n brush = bpy.context.tool_settings.image_paint.brush\r\n if brush:\r\n brush.secondary_color = color\r\n\r\n\r\ndef configure_paint_brush(\r\n strength: float = 1.0,\r\n radius: int = 50,\r\n blend_mode: str = 'MIX',\r\n use_pressure: bool = True\r\n) -> None:\r\n \"\"\"\r\n Configure paint brush settings.\r\n \r\n Args:\r\n strength: Brush strength\r\n radius: Brush size\r\n blend_mode: 'MIX', 'DARKEN', 'MULTIPLY', 'LIGHTEN', 'SCREEN', 'ADD', 'SUB'\r\n use_pressure: Enable pressure sensitivity\r\n \"\"\"\r\n brush = bpy.context.tool_settings.image_paint.brush\r\n if brush:\r\n brush.strength = strength\r\n brush.size = radius\r\n brush.blend = blend_mode\r\n brush.use_pressure_strength = use_pressure\r\n\r\n\r\ndef enable_stencil_mapping(\r\n image_path: str,\r\n scale: float = 1.0\r\n) -> None:\r\n \"\"\"Enable stencil brush mapping.\"\"\"\r\n img = bpy.data.images.load(image_path)\r\n brush = bpy.context.tool_settings.image_paint.brush\r\n if brush:\r\n brush.texture_slot.map_mode = 'STENCIL'\r\n tex = bpy.data.textures.new(\"StencilTex\", 'IMAGE')\r\n tex.image = img\r\n brush.texture = tex\r\n\r\n\r\ndef save_painted_image(\r\n image: bpy.types.Image,\r\n filepath: str = None\r\n) -> None:\r\n \"\"\"\r\n Save painted image to file.\r\n \r\n Args:\r\n image: Image to save\r\n filepath: Path (uses image path if None)\r\n \"\"\"\r\n if filepath:\r\n image.filepath_raw = filepath\r\n image.save()\r\n\r\n\r\ndef pack_painted_images() -> None:\r\n \"\"\"Pack all painted images into blend file.\"\"\"\r\n for img in bpy.data.images:\r\n if img.is_dirty:\r\n img.pack()\r\n\r\n\r\ndef bake_texture(\r\n obj: bpy.types.Object,\r\n bake_type: str = 'DIFFUSE',\r\n width: int = 1024,\r\n height: int = 1024,\r\n margin: int = 16,\r\n output_path: str = None\r\n) -> bpy.types.Image:\r\n \"\"\"\r\n Bake texture from materials.\r\n \r\n Args:\r\n obj: Target object\r\n bake_type: 'DIFFUSE', 'ROUGHNESS', 'NORMAL', 'AO', 'COMBINED'\r\n width: Output width\r\n height: Output height\r\n margin: UV margin pixels\r\n output_path: Save path\r\n \r\n Returns:\r\n Baked image\r\n \"\"\"\r\n # Create target image\r\n img_name = f\"{obj.name}_{bake_type}\"\r\n img = bpy.data.images.new(img_name, width, height)\r\n \r\n # Set up material for baking\r\n for mat_slot in obj.material_slots:\r\n if mat_slot.material and mat_slot.material.use_nodes:\r\n nodes = mat_slot.material.node_tree.nodes\r\n \r\n # Add image texture node\r\n tex_node = nodes.new('ShaderNodeTexImage')\r\n tex_node.image = img\r\n tex_node.select = True\r\n nodes.active = tex_node\r\n \r\n # Configure bake settings\r\n bpy.context.scene.render.engine = 'CYCLES'\r\n bpy.context.scene.render.bake.margin = margin\r\n \r\n # Select object\r\n bpy.ops.object.select_all(action='DESELECT')\r\n obj.select_set(True)\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n # Bake\r\n bpy.ops.object.bake(type=bake_type)\r\n \r\n if output_path:\r\n img.filepath_raw = output_path\r\n img.save()\r\n \r\n return img\r\n\r\n\r\ndef set_paint_canvas(image: bpy.types.Image) -> None:\r\n \"\"\"Set active painting canvas.\"\"\"\r\n ts = bpy.context.tool_settings.image_paint\r\n ts.canvas = image\r\n ts.mode = 'IMAGE'\r\n\r\n\r\ndef toggle_symmetry(\r\n x: bool = True,\r\n y: bool = False,\r\n z: bool = False\r\n) -> None:\r\n \"\"\"Toggle paint symmetry.\"\"\"\r\n paint = bpy.context.tool_settings.image_paint\r\n paint.use_symmetry_x = x\r\n paint.use_symmetry_y = y\r\n paint.use_symmetry_z = z","category":"utility","source_file":"texture_paint_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Enable Texture Paint Mode. Category: utility","output":"import bpy\n\ndef enable_texture_paint_mode(obj: bpy.types.Object = None) -> None:\r\n \"\"\"Enter texture paint mode.\"\"\"\r\n if obj:\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='TEXTURE_PAINT')\r\n\r\n\r\ndef create_paint_slot(\r\n obj: bpy.types.Object,\r\n slot_type: str = 'DIFFUSE',\r\n width: int = 1024,\r\n height: int = 1024,\r\n color: tuple = (0.5, 0.5, 0.5, 1.0),\r\n name: str = \"PaintTexture\"\r\n) -> bpy.types.Image:\r\n \"\"\"\r\n Create texture paint slot.\r\n \r\n Args:\r\n obj: Target object\r\n slot_type: 'DIFFUSE', 'ROUGHNESS', 'NORMAL', 'BUMP'\r\n width: Image width\r\n height: Image height\r\n color: Fill color\r\n name: Texture name\r\n \r\n Returns:\r\n Created image\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='TEXTURE_PAINT')\r\n \r\n bpy.ops.paint.add_texture_paint_slot(\r\n type=slot_type,\r\n width=width,\r\n height=height,\r\n color=color,\r\n name=name\r\n )\r\n \r\n return bpy.data.images.get(name)\r\n\r\n\r\ndef set_paint_brush(brush_name: str) -> None:\r\n \"\"\"\r\n Set active paint brush.\r\n \r\n Args:\r\n brush_name: 'TexDraw', 'Soften', 'Smear', 'Clone', 'Fill', 'Mask'\r\n \"\"\"\r\n brush = bpy.data.brushes.get(brush_name)\r\n if brush:\r\n bpy.context.tool_settings.image_paint.brush = brush\r\n\r\n\r\ndef set_brush_color(color: tuple = (1.0, 1.0, 1.0)) -> None:\r\n \"\"\"Set brush primary color.\"\"\"\r\n brush = bpy.context.tool_settings.image_paint.brush\r\n if brush:\r\n brush.color = color\r\n\r\n\r\ndef set_brush_secondary_color(color: tuple = (0.0, 0.0, 0.0)) -> None:\r\n \"\"\"Set brush secondary color.\"\"\"\r\n brush = bpy.context.tool_settings.image_paint.brush\r\n if brush:\r\n brush.secondary_color = color\r\n\r\n\r\ndef configure_paint_brush(\r\n strength: float = 1.0,\r\n radius: int = 50,\r\n blend_mode: str = 'MIX',\r\n use_pressure: bool = True\r\n) -> None:\r\n \"\"\"\r\n Configure paint brush settings.\r\n \r\n Args:\r\n strength: Brush strength\r\n radius: Brush size\r\n blend_mode: 'MIX', 'DARKEN', 'MULTIPLY', 'LIGHTEN', 'SCREEN', 'ADD', 'SUB'\r\n use_pressure: Enable pressure sensitivity\r\n \"\"\"\r\n brush = bpy.context.tool_settings.image_paint.brush\r\n if brush:\r\n brush.strength = strength\r\n brush.size = radius\r\n brush.blend = blend_mode\r\n brush.use_pressure_strength = use_pressure\r\n\r\n\r\ndef enable_stencil_mapping(\r\n image_path: str,\r\n scale: float = 1.0\r\n) -> None:\r\n \"\"\"Enable stencil brush mapping.\"\"\"\r\n img = bpy.data.images.load(image_path)\r\n brush = bpy.context.tool_settings.image_paint.brush\r\n if brush:\r\n brush.texture_slot.map_mode = 'STENCIL'\r\n tex = bpy.data.textures.new(\"StencilTex\", 'IMAGE')\r\n tex.image = img\r\n brush.texture = tex\r\n\r\n\r\ndef save_painted_image(\r\n image: bpy.types.Image,\r\n filepath: str = None\r\n) -> None:\r\n \"\"\"\r\n Save painted image to file.\r\n \r\n Args:\r\n image: Image to save\r\n filepath: Path (uses image path if None)\r\n \"\"\"\r\n if filepath:\r\n image.filepath_raw = filepath\r\n image.save()\r\n\r\n\r\ndef pack_painted_images() -> None:\r\n \"\"\"Pack all painted images into blend file.\"\"\"\r\n for img in bpy.data.images:\r\n if img.is_dirty:\r\n img.pack()\r\n\r\n\r\ndef bake_texture(\r\n obj: bpy.types.Object,\r\n bake_type: str = 'DIFFUSE',\r\n width: int = 1024,\r\n height: int = 1024,\r\n margin: int = 16,\r\n output_path: str = None\r\n) -> bpy.types.Image:\r\n \"\"\"\r\n Bake texture from materials.\r\n \r\n Args:\r\n obj: Target object\r\n bake_type: 'DIFFUSE', 'ROUGHNESS', 'NORMAL', 'AO', 'COMBINED'\r\n width: Output width\r\n height: Output height\r\n margin: UV margin pixels\r\n output_path: Save path\r\n \r\n Returns:\r\n Baked image\r\n \"\"\"\r\n # Create target image\r\n img_name = f\"{obj.name}_{bake_type}\"\r\n img = bpy.data.images.new(img_name, width, height)\r\n \r\n # Set up material for baking\r\n for mat_slot in obj.material_slots:\r\n if mat_slot.material and mat_slot.material.use_nodes:\r\n nodes = mat_slot.material.node_tree.nodes\r\n \r\n # Add image texture node\r\n tex_node = nodes.new('ShaderNodeTexImage')\r\n tex_node.image = img\r\n tex_node.select = True\r\n nodes.active = tex_node\r\n \r\n # Configure bake settings\r\n bpy.context.scene.render.engine = 'CYCLES'\r\n bpy.context.scene.render.bake.margin = margin\r\n \r\n # Select object\r\n bpy.ops.object.select_all(action='DESELECT')\r\n obj.select_set(True)\r\n bpy.context.view_layer.objects.active = obj\r\n \r\n # Bake\r\n bpy.ops.object.bake(type=bake_type)\r\n \r\n if output_path:\r\n img.filepath_raw = output_path\r\n img.save()\r\n \r\n return img\r\n\r\n\r\ndef set_paint_canvas(image: bpy.types.Image) -> None:\r\n \"\"\"Set active painting canvas.\"\"\"\r\n ts = bpy.context.tool_settings.image_paint\r\n ts.canvas = image\r\n ts.mode = 'IMAGE'\r\n\r\n\r\ndef toggle_symmetry(\r\n x: bool = True,\r\n y: bool = False,\r\n z: bool = False\r\n) -> None:\r\n \"\"\"Toggle paint symmetry.\"\"\"\r\n paint = bpy.context.tool_settings.image_paint\r\n paint.use_symmetry_x = x\r\n paint.use_symmetry_y = y\r\n paint.use_symmetry_z = z","category":"utility","source_file":"texture_paint_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: 3D Text Utilities. Functions for creating and styling 3D text objects.","output":"import bpy\r\n\r\n\r\ndef create_3d_text(\r\n text: str = \"Text\",\r\n location: tuple = (0, 0, 0),\r\n size: float = 1.0,\r\n extrude: float = 0.1,\r\n font: str = None,\r\n name: str = \"Text\"\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create a 3D text object.\r\n \r\n Args:\r\n text: Text content\r\n location: Position\r\n size: Text size\r\n extrude: 3D depth\r\n font: Path to font file (uses default if None)\r\n name: Object name\r\n \r\n Returns:\r\n The created text object\r\n \"\"\"\r\n bpy.ops.object.text_add(location=location)\r\n text_obj = bpy.context.active_object\r\n text_obj.name = name\r\n \r\n text_obj.data.body = text\r\n text_obj.data.size = size\r\n text_obj.data.extrude = extrude\r\n \r\n if font and font.endswith(('.ttf', '.otf')):\r\n text_obj.data.font = bpy.data.fonts.load(font)\r\n \r\n return text_obj\r\n\r\n\r\ndef set_text_alignment(\r\n text_obj: bpy.types.Object,\r\n horizontal: str = 'CENTER',\r\n vertical: str = 'CENTER'\r\n) -> None:\r\n \"\"\"\r\n Set text alignment.\r\n \r\n Args:\r\n text_obj: Text object\r\n horizontal: 'LEFT', 'CENTER', 'RIGHT', 'JUSTIFY', 'FLUSH'\r\n vertical: 'TOP', 'CENTER', 'BOTTOM'\r\n \"\"\"\r\n text_obj.data.align_x = horizontal\r\n text_obj.data.align_y = vertical\r\n\r\n\r\ndef set_text_bevel(\r\n text_obj: bpy.types.Object,\r\n depth: float = 0.02,\r\n resolution: int = 4\r\n) -> None:\r\n \"\"\"Add bevel to text edges.\"\"\"\r\n text_obj.data.bevel_depth = depth\r\n text_obj.data.bevel_resolution = resolution\r\n\r\n\r\ndef set_text_spacing(\r\n text_obj: bpy.types.Object,\r\n character: float = 1.0,\r\n word: float = 1.0,\r\n line: float = 1.0\r\n) -> None:\r\n \"\"\"Set text spacing.\"\"\"\r\n text_obj.data.space_character = character\r\n text_obj.data.space_word = word\r\n text_obj.data.space_line = line\r\n\r\n\r\ndef text_to_mesh(text_obj: bpy.types.Object) -> bpy.types.Object:\r\n \"\"\"Convert text to mesh.\"\"\"\r\n bpy.context.view_layer.objects.active = text_obj\r\n text_obj.select_set(True)\r\n bpy.ops.object.convert(target='MESH')\r\n return bpy.context.active_object\r\n\r\n\r\ndef text_to_curve(text_obj: bpy.types.Object) -> bpy.types.Object:\r\n \"\"\"Convert text to curve.\"\"\"\r\n bpy.context.view_layer.objects.active = text_obj\r\n text_obj.select_set(True)\r\n bpy.ops.object.convert(target='CURVE')\r\n return bpy.context.active_object\r\n\r\n\r\ndef create_title_text(\r\n text: str,\r\n style: str = 'BOLD',\r\n color: tuple = (1, 1, 1),\r\n location: tuple = (0, 0, 0)\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create styled title text with material.\r\n \r\n Args:\r\n text: Title content\r\n style: 'BOLD', 'OUTLINE', 'SHADOW'\r\n color: RGB color\r\n location: Position\r\n \r\n Returns:\r\n Text object with material\r\n \"\"\"\r\n if style == 'BOLD':\r\n extrude, bevel = 0.15, 0.02\r\n elif style == 'OUTLINE':\r\n extrude, bevel = 0.02, 0.01\r\n else:\r\n extrude, bevel = 0.1, 0.01\r\n \r\n text_obj = create_3d_text(\r\n text=text,\r\n location=location,\r\n extrude=extrude,\r\n name=f\"Title_{text[:10]}\"\r\n )\r\n \r\n set_text_bevel(text_obj, depth=bevel, resolution=3)\r\n set_text_alignment(text_obj, 'CENTER', 'CENTER')\r\n \r\n # Add material\r\n mat = bpy.data.materials.new(f\"TitleMat_{text[:10]}\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.3\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n text_obj.data.materials.append(mat)\r\n \r\n return text_obj","category":"utility","source_file":"text_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Text To Mesh. Category: utility","output":"import bpy\n\ndef text_to_mesh(text_obj: bpy.types.Object) -> bpy.types.Object:\r\n \"\"\"Convert text to mesh.\"\"\"\r\n bpy.context.view_layer.objects.active = text_obj\r\n text_obj.select_set(True)\r\n bpy.ops.object.convert(target='MESH')\r\n return bpy.context.active_object\r\n\r\n\r\ndef text_to_curve(text_obj: bpy.types.Object) -> bpy.types.Object:\r\n \"\"\"Convert text to curve.\"\"\"\r\n bpy.context.view_layer.objects.active = text_obj\r\n text_obj.select_set(True)\r\n bpy.ops.object.convert(target='CURVE')\r\n return bpy.context.active_object\r\n\r\n\r\ndef create_title_text(\r\n text: str,\r\n style: str = 'BOLD',\r\n color: tuple = (1, 1, 1),\r\n location: tuple = (0, 0, 0)\r\n) -> bpy.types.Object:\r\n \"\"\"\r\n Create styled title text with material.\r\n \r\n Args:\r\n text: Title content\r\n style: 'BOLD', 'OUTLINE', 'SHADOW'\r\n color: RGB color\r\n location: Position\r\n \r\n Returns:\r\n Text object with material\r\n \"\"\"\r\n if style == 'BOLD':\r\n extrude, bevel = 0.15, 0.02\r\n elif style == 'OUTLINE':\r\n extrude, bevel = 0.02, 0.01\r\n else:\r\n extrude, bevel = 0.1, 0.01\r\n \r\n text_obj = create_3d_text(\r\n text=text,\r\n location=location,\r\n extrude=extrude,\r\n name=f\"Title_{text[:10]}\"\r\n )\r\n \r\n set_text_bevel(text_obj, depth=bevel, resolution=3)\r\n set_text_alignment(text_obj, 'CENTER', 'CENTER')\r\n \r\n # Add material\r\n mat = bpy.data.materials.new(f\"TitleMat_{text[:10]}\")\r\n bsdf = mat.node_tree.nodes.get(\"Principled BSDF\")\r\n bsdf.inputs['Base Color'].default_value = (*color, 1.0)\r\n bsdf.inputs['Metallic'].default_value = 0.3\r\n bsdf.inputs['Roughness'].default_value = 0.4\r\n text_obj.data.materials.append(mat)\r\n \r\n return text_obj","category":"utility","source_file":"text_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Timeline and Playback Utilities. Functions for controlling timeline and playback settings.","output":"import bpy\r\n\r\n\r\ndef set_frame_range(start: int, end: int) -> None:\r\n \"\"\"Set scene frame range.\"\"\"\r\n scene = bpy.context.scene\r\n scene.frame_start = start\r\n scene.frame_end = end\r\n\r\n\r\ndef set_fps(fps: float, base: int = 1) -> None:\r\n \"\"\"\r\n Set scene framerate.\r\n \r\n Args:\r\n fps: Frames per second\r\n base: Frame base (usually 1)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.render.fps = int(fps)\r\n scene.render.fps_base = base\r\n\r\n\r\ndef set_current_frame(frame: int) -> None:\r\n \"\"\"Jump to specific frame.\"\"\"\r\n bpy.context.scene.frame_set(frame)\r\n\r\n\r\ndef get_current_frame() -> int:\r\n \"\"\"Get current frame number.\"\"\"\r\n return bpy.context.scene.frame_current\r\n\r\n\r\ndef play_animation() -> None:\r\n \"\"\"Start animation playback.\"\"\"\r\n bpy.ops.screen.animation_play()\r\n\r\n\r\ndef stop_animation() -> None:\r\n \"\"\"Stop animation playback.\"\"\"\r\n bpy.ops.screen.animation_cancel()\r\n\r\n\r\ndef toggle_playback() -> None:\r\n \"\"\"Toggle animation playback.\"\"\"\r\n if bpy.context.screen.is_animation_playing:\r\n stop_animation()\r\n else:\r\n play_animation()\r\n\r\n\r\ndef jump_to_start() -> None:\r\n \"\"\"Jump to start frame.\"\"\"\r\n bpy.context.scene.frame_set(bpy.context.scene.frame_start)\r\n\r\n\r\ndef jump_to_end() -> None:\r\n \"\"\"Jump to end frame.\"\"\"\r\n bpy.context.scene.frame_set(bpy.context.scene.frame_end)\r\n\r\n\r\ndef set_preview_range(start: int, end: int) -> None:\r\n \"\"\"Set preview range for playback.\"\"\"\r\n scene = bpy.context.scene\r\n scene.use_preview_range = True\r\n scene.frame_preview_start = start\r\n scene.frame_preview_end = end\r\n\r\n\r\ndef clear_preview_range() -> None:\r\n \"\"\"Clear preview range.\"\"\"\r\n bpy.context.scene.use_preview_range = False\r\n\r\n\r\ndef set_playback_sync(mode: str = 'AUDIO_SYNC') -> None:\r\n \"\"\"\r\n Set playback sync mode.\r\n \r\n Args:\r\n mode: 'NONE', 'FRAME_DROP', 'AUDIO_SYNC'\r\n \"\"\"\r\n bpy.context.scene.sync_mode = mode\r\n\r\n\r\ndef enable_audio_scrubbing(enabled: bool = True) -> None:\r\n \"\"\"Enable/disable audio scrubbing.\"\"\"\r\n bpy.context.scene.use_audio_scrub = enabled\r\n\r\n\r\ndef set_audio_volume(volume: float = 1.0) -> None:\r\n \"\"\"Set scene audio volume (0-1).\"\"\"\r\n bpy.context.scene.audio_volume = volume\r\n\r\n\r\ndef mute_audio(mute: bool = True) -> None:\r\n \"\"\"Mute/unmute scene audio.\"\"\"\r\n bpy.context.scene.use_audio = not mute\r\n\r\n\r\ndef go_to_next_keyframe() -> None:\r\n \"\"\"Jump to next keyframe.\"\"\"\r\n bpy.ops.screen.keyframe_jump(next=True)\r\n\r\n\r\ndef go_to_prev_keyframe() -> None:\r\n \"\"\"Jump to previous keyframe.\"\"\"\r\n bpy.ops.screen.keyframe_jump(next=False)\r\n\r\n\r\ndef go_to_next_marker() -> None:\r\n \"\"\"Jump to next marker.\"\"\"\r\n bpy.ops.screen.marker_jump(next=True)\r\n\r\n\r\ndef go_to_prev_marker() -> None:\r\n \"\"\"Jump to previous marker.\"\"\"\r\n bpy.ops.screen.marker_jump(next=False)\r\n\r\n\r\ndef add_marker(name: str = \"\", frame: int = None) -> bpy.types.TimelineMarker:\r\n \"\"\"\r\n Add timeline marker.\r\n \r\n Args:\r\n name: Marker name\r\n frame: Frame number (current if None)\r\n \r\n Returns:\r\n Created marker\r\n \"\"\"\r\n if frame is None:\r\n frame = bpy.context.scene.frame_current\r\n \r\n marker = bpy.context.scene.timeline_markers.new(name, frame=frame)\r\n return marker\r\n\r\n\r\ndef remove_marker(name: str) -> bool:\r\n \"\"\"Remove marker by name.\"\"\"\r\n marker = bpy.context.scene.timeline_markers.get(name)\r\n if marker:\r\n bpy.context.scene.timeline_markers.remove(marker)\r\n return True\r\n return False\r\n\r\n\r\ndef list_markers() -> list:\r\n \"\"\"List all markers with their frames.\"\"\"\r\n return [(m.name, m.frame) for m in bpy.context.scene.timeline_markers]\r\n\r\n\r\ndef clear_all_markers() -> None:\r\n \"\"\"Remove all timeline markers.\"\"\"\r\n bpy.context.scene.timeline_markers.clear()\r\n\r\n\r\ndef set_frame_step(step: int = 1) -> None:\r\n \"\"\"Set frame step for stepping through animation.\"\"\"\r\n bpy.context.scene.frame_step = step\r\n\r\n\r\ndef bind_camera_to_marker(\r\n marker: bpy.types.TimelineMarker,\r\n camera: bpy.types.Object\r\n) -> None:\r\n \"\"\"Bind camera to marker for camera switching.\"\"\"\r\n marker.camera = camera","category":"utility","source_file":"timeline_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Set Frame Range. Category: utility","output":"import bpy\n\ndef set_frame_range(start: int, end: int) -> None:\r\n \"\"\"Set scene frame range.\"\"\"\r\n scene = bpy.context.scene\r\n scene.frame_start = start\r\n scene.frame_end = end\r\n\r\n\r\ndef set_fps(fps: float, base: int = 1) -> None:\r\n \"\"\"\r\n Set scene framerate.\r\n \r\n Args:\r\n fps: Frames per second\r\n base: Frame base (usually 1)\r\n \"\"\"\r\n scene = bpy.context.scene\r\n scene.render.fps = int(fps)\r\n scene.render.fps_base = base\r\n\r\n\r\ndef set_current_frame(frame: int) -> None:\r\n \"\"\"Jump to specific frame.\"\"\"\r\n bpy.context.scene.frame_set(frame)\r\n\r\n\r\ndef get_current_frame() -> int:\r\n \"\"\"Get current frame number.\"\"\"\r\n return bpy.context.scene.frame_current\r\n\r\n\r\ndef play_animation() -> None:\r\n \"\"\"Start animation playback.\"\"\"\r\n bpy.ops.screen.animation_play()\r\n\r\n\r\ndef stop_animation() -> None:\r\n \"\"\"Stop animation playback.\"\"\"\r\n bpy.ops.screen.animation_cancel()\r\n\r\n\r\ndef toggle_playback() -> None:\r\n \"\"\"Toggle animation playback.\"\"\"\r\n if bpy.context.screen.is_animation_playing:\r\n stop_animation()\r\n else:\r\n play_animation()\r\n\r\n\r\ndef jump_to_start() -> None:\r\n \"\"\"Jump to start frame.\"\"\"\r\n bpy.context.scene.frame_set(bpy.context.scene.frame_start)\r\n\r\n\r\ndef jump_to_end() -> None:\r\n \"\"\"Jump to end frame.\"\"\"\r\n bpy.context.scene.frame_set(bpy.context.scene.frame_end)\r\n\r\n\r\ndef set_preview_range(start: int, end: int) -> None:\r\n \"\"\"Set preview range for playback.\"\"\"\r\n scene = bpy.context.scene\r\n scene.use_preview_range = True\r\n scene.frame_preview_start = start\r\n scene.frame_preview_end = end\r\n\r\n\r\ndef clear_preview_range() -> None:\r\n \"\"\"Clear preview range.\"\"\"\r\n bpy.context.scene.use_preview_range = False\r\n\r\n\r\ndef set_playback_sync(mode: str = 'AUDIO_SYNC') -> None:\r\n \"\"\"\r\n Set playback sync mode.\r\n \r\n Args:\r\n mode: 'NONE', 'FRAME_DROP', 'AUDIO_SYNC'\r\n \"\"\"\r\n bpy.context.scene.sync_mode = mode\r\n\r\n\r\ndef enable_audio_scrubbing(enabled: bool = True) -> None:\r\n \"\"\"Enable/disable audio scrubbing.\"\"\"\r\n bpy.context.scene.use_audio_scrub = enabled\r\n\r\n\r\ndef set_audio_volume(volume: float = 1.0) -> None:\r\n \"\"\"Set scene audio volume (0-1).\"\"\"\r\n bpy.context.scene.audio_volume = volume\r\n\r\n\r\ndef mute_audio(mute: bool = True) -> None:\r\n \"\"\"Mute/unmute scene audio.\"\"\"\r\n bpy.context.scene.use_audio = not mute\r\n\r\n\r\ndef go_to_next_keyframe() -> None:\r\n \"\"\"Jump to next keyframe.\"\"\"\r\n bpy.ops.screen.keyframe_jump(next=True)\r\n\r\n\r\ndef go_to_prev_keyframe() -> None:\r\n \"\"\"Jump to previous keyframe.\"\"\"\r\n bpy.ops.screen.keyframe_jump(next=False)\r\n\r\n\r\ndef go_to_next_marker() -> None:\r\n \"\"\"Jump to next marker.\"\"\"\r\n bpy.ops.screen.marker_jump(next=True)\r\n\r\n\r\ndef go_to_prev_marker() -> None:\r\n \"\"\"Jump to previous marker.\"\"\"\r\n bpy.ops.screen.marker_jump(next=False)\r\n\r\n\r\ndef add_marker(name: str = \"\", frame: int = None) -> bpy.types.TimelineMarker:\r\n \"\"\"\r\n Add timeline marker.\r\n \r\n Args:\r\n name: Marker name\r\n frame: Frame number (current if None)\r\n \r\n Returns:\r\n Created marker\r\n \"\"\"\r\n if frame is None:\r\n frame = bpy.context.scene.frame_current\r\n \r\n marker = bpy.context.scene.timeline_markers.new(name, frame=frame)\r\n return marker\r\n\r\n\r\ndef remove_marker(name: str) -> bool:\r\n \"\"\"Remove marker by name.\"\"\"\r\n marker = bpy.context.scene.timeline_markers.get(name)\r\n if marker:\r\n bpy.context.scene.timeline_markers.remove(marker)\r\n return True\r\n return False\r\n\r\n\r\ndef list_markers() -> list:\r\n \"\"\"List all markers with their frames.\"\"\"\r\n return [(m.name, m.frame) for m in bpy.context.scene.timeline_markers]\r\n\r\n\r\ndef clear_all_markers() -> None:\r\n \"\"\"Remove all timeline markers.\"\"\"\r\n bpy.context.scene.timeline_markers.clear()\r\n\r\n\r\ndef set_frame_step(step: int = 1) -> None:\r\n \"\"\"Set frame step for stepping through animation.\"\"\"\r\n bpy.context.scene.frame_step = step\r\n\r\n\r\ndef bind_camera_to_marker(\r\n marker: bpy.types.TimelineMarker,\r\n camera: bpy.types.Object\r\n) -> None:\r\n \"\"\"Bind camera to marker for camera switching.\"\"\"\r\n marker.camera = camera","category":"utility","source_file":"timeline_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Object Transformations. Modifying object location, rotation, and scale properties directly.","output":"import bpy\r\nimport math\r\n\r\ndef move_object(obj, location=(0, 0, 0)):\r\n obj.location = location\r\n\r\ndef rotate_object(obj, rotation_degrees=(0, 0, 0)):\r\n # Blender uses radians for rotation_euler\r\n obj.rotation_euler[0] = math.radians(rotation_degrees[0])\r\n obj.rotation_euler[1] = math.radians(rotation_degrees[1])\r\n obj.rotation_euler[2] = math.radians(rotation_degrees[2])\r\n\r\ndef scale_object(obj, scale=(1, 1, 1)):\r\n obj.scale = scale\r\n\r\ndef apply_transforms(obj, location=True, rotation=True, scale=True):\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.transform_apply(location=location, rotation=rotation, scale=scale)","category":"utility","source_file":"transform_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Move Object. Category: utility","output":"import bpy\n\ndef move_object(obj, location=(0, 0, 0)):\n obj.location = location","category":"utility","source_file":"transform_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Rotate Object. Category: utility","output":"import bpy\n\ndef rotate_object(obj, rotation_degrees=(0, 0, 0)):\n # Blender uses radians for rotation_euler\r\n obj.rotation_euler[0] = math.radians(rotation_degrees[0])\r\n obj.rotation_euler[1] = math.radians(rotation_degrees[1])\r\n obj.rotation_euler[2] = math.radians(rotation_degrees[2])","category":"utility","source_file":"transform_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Scale Object. Category: utility","output":"import bpy\n\ndef scale_object(obj, scale=(1, 1, 1)):\n obj.scale = scale","category":"utility","source_file":"transform_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Apply Transforms. Category: utility","output":"import bpy\n\ndef apply_transforms(obj, location=True, rotation=True, scale=True):\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.transform_apply(location=location, rotation=rotation, scale=scale)","category":"utility","source_file":"transform_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: UV Mapping Utilities. Utility functions for UV unwrapping and texture coordinate management in Blender.","output":"import bpy\r\nimport bmesh\r\nimport math\r\n\r\n\r\ndef smart_uv_project(\r\n obj: bpy.types.Object,\r\n angle_limit: float = 66.0,\r\n island_margin: float = 0.02,\r\n scale_to_bounds: bool = True\r\n) -> None:\r\n \"\"\"\r\n Apply smart UV projection to an object.\r\n \r\n Args:\r\n obj: Object to unwrap\r\n angle_limit: Angle for splitting in degrees\r\n island_margin: Space between UV islands\r\n scale_to_bounds: Scale UVs to fit 0-1 space\r\n \r\n Example:\r\n >>> smart_uv_project(complex_mesh, angle_limit=45)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n \r\n bpy.ops.uv.smart_project(\r\n angle_limit=math.radians(angle_limit),\r\n island_margin=island_margin,\r\n scale_to_bounds=scale_to_bounds\r\n )\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef cube_project(\r\n obj: bpy.types.Object,\r\n cube_size: float = 1.0,\r\n correct_aspect: bool = True\r\n) -> None:\r\n \"\"\"\r\n Apply cube projection UV mapping.\r\n \r\n Best for: Box-like objects, buildings, rooms\r\n \r\n Args:\r\n obj: Object to unwrap\r\n cube_size: Scale of the projection\r\n correct_aspect: Correct for non-square faces\r\n \r\n Example:\r\n >>> cube_project(building_mesh, cube_size=2.0)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n \r\n bpy.ops.uv.cube_project(\r\n cube_size=cube_size,\r\n correct_aspect=correct_aspect\r\n )\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef cylinder_project(\r\n obj: bpy.types.Object,\r\n direction: str = 'Z',\r\n radius: float = 1.0\r\n) -> None:\r\n \"\"\"\r\n Apply cylindrical projection UV mapping.\r\n \r\n Best for: Cylindrical objects, bottles, poles\r\n \r\n Args:\r\n obj: Object to unwrap\r\n direction: Cylinder axis ('X', 'Y', 'Z')\r\n radius: Cylinder radius\r\n \r\n Example:\r\n >>> cylinder_project(bottle_mesh, direction='Z')\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n \r\n bpy.ops.uv.cylinder_project(\r\n direction=direction,\r\n radius=radius\r\n )\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef sphere_project(\r\n obj: bpy.types.Object,\r\n direction: str = 'VIEW_ON_EQUATOR'\r\n) -> None:\r\n \"\"\"\r\n Apply spherical projection UV mapping.\r\n \r\n Best for: Spheres, planets, globes\r\n \r\n Args:\r\n obj: Object to unwrap\r\n direction: Projection direction\r\n \r\n Example:\r\n >>> sphere_project(planet_mesh)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n \r\n bpy.ops.uv.sphere_project(direction=direction)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef unwrap_with_seams(\r\n obj: bpy.types.Object,\r\n seam_angle: float = 30.0,\r\n margin: float = 0.02\r\n) -> None:\r\n \"\"\"\r\n Automatically mark seams based on sharp edges and unwrap.\r\n \r\n Args:\r\n obj: Object to unwrap\r\n seam_angle: Minimum angle for seam (degrees)\r\n margin: UV island margin\r\n \r\n Example:\r\n >>> unwrap_with_seams(character_mesh, seam_angle=45)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n \r\n # Clear existing seams\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.mesh.mark_seam(clear=True)\r\n \r\n # Mark seams from sharp edges\r\n bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(seam_angle))\r\n bpy.ops.mesh.mark_seam()\r\n \r\n # Unwrap\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=margin)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef pack_islands(\r\n obj: bpy.types.Object,\r\n margin: float = 0.02,\r\n rotate: bool = True\r\n) -> None:\r\n \"\"\"\r\n Pack UV islands to minimize unused space.\r\n \r\n Args:\r\n obj: Object with UVs to pack\r\n margin: Space between islands\r\n rotate: Allow rotation for better packing\r\n \r\n Example:\r\n >>> pack_islands(model, margin=0.01)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n \r\n bpy.ops.uv.pack_islands(margin=margin, rotate=rotate)\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef average_islands_scale(obj: bpy.types.Object) -> None:\r\n \"\"\"\r\n Equalize the scale of all UV islands for consistent texel density.\r\n \r\n Args:\r\n obj: Object with UVs to adjust\r\n \r\n Example:\r\n >>> average_islands_scale(game_asset)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n \r\n bpy.ops.uv.average_islands_scale()\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef add_uv_layer(\r\n obj: bpy.types.Object,\r\n name: str = \"UVMap_Alt\"\r\n) -> bpy.types.MeshUVLoopLayer:\r\n \"\"\"\r\n Add a new UV layer to an object.\r\n \r\n Args:\r\n obj: Target object\r\n name: UV layer name\r\n \r\n Returns:\r\n The created UV layer\r\n \r\n Example:\r\n >>> lightmap_uv = add_uv_layer(model, \"Lightmap\")\r\n \"\"\"\r\n mesh = obj.data\r\n uv_layer = mesh.uv_layers.new(name=name)\r\n return uv_layer\r\n\r\n\r\ndef set_active_uv_layer(\r\n obj: bpy.types.Object,\r\n layer_name: str\r\n) -> bool:\r\n \"\"\"\r\n Set the active UV layer by name.\r\n \r\n Args:\r\n obj: Target object\r\n layer_name: Name of UV layer to activate\r\n \r\n Returns:\r\n True if layer was found and activated\r\n \"\"\"\r\n mesh = obj.data\r\n for uv_layer in mesh.uv_layers:\r\n if uv_layer.name == layer_name:\r\n mesh.uv_layers.active = uv_layer\r\n return True\r\n return False","category":"utility","source_file":"uv_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Average Islands Scale. Category: utility","output":"import bpy\n\ndef average_islands_scale(obj: bpy.types.Object) -> None:\r\n \"\"\"\r\n Equalize the scale of all UV islands for consistent texel density.\r\n \r\n Args:\r\n obj: Object with UVs to adjust\r\n \r\n Example:\r\n >>> average_islands_scale(game_asset)\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='EDIT')\r\n bpy.ops.mesh.select_all(action='SELECT')\r\n \r\n bpy.ops.uv.average_islands_scale()\r\n \r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef add_uv_layer(\r\n obj: bpy.types.Object,\r\n name: str = \"UVMap_Alt\"\r\n) -> bpy.types.MeshUVLoopLayer:\r\n \"\"\"\r\n Add a new UV layer to an object.\r\n \r\n Args:\r\n obj: Target object\r\n name: UV layer name\r\n \r\n Returns:\r\n The created UV layer\r\n \r\n Example:\r\n >>> lightmap_uv = add_uv_layer(model, \"Lightmap\")\r\n \"\"\"\r\n mesh = obj.data\r\n uv_layer = mesh.uv_layers.new(name=name)\r\n return uv_layer\r\n\r\n\r\ndef set_active_uv_layer(\r\n obj: bpy.types.Object,\r\n layer_name: str\r\n) -> bool:\r\n \"\"\"\r\n Set the active UV layer by name.\r\n \r\n Args:\r\n obj: Target object\r\n layer_name: Name of UV layer to activate\r\n \r\n Returns:\r\n True if layer was found and activated\r\n \"\"\"\r\n mesh = obj.data\r\n for uv_layer in mesh.uv_layers:\r\n if uv_layer.name == layer_name:\r\n mesh.uv_layers.active = uv_layer\r\n return True\r\n return False","category":"utility","source_file":"uv_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Vertex Group Utilities. Functions for creating and managing vertex groups for mesh deformation.","output":"import bpy\r\n\r\n\r\ndef create_vertex_group(\r\n obj: bpy.types.Object,\r\n name: str\r\n) -> bpy.types.VertexGroup:\r\n \"\"\"Create a new vertex group.\"\"\"\r\n return obj.vertex_groups.new(name=name)\r\n\r\n\r\ndef add_vertices_to_group(\r\n obj: bpy.types.Object,\r\n group_name: str,\r\n vertex_indices: list,\r\n weight: float = 1.0\r\n) -> None:\r\n \"\"\"\r\n Add vertices to a vertex group.\r\n \r\n Args:\r\n obj: Mesh object\r\n group_name: Vertex group name\r\n vertex_indices: List of vertex indices\r\n weight: Weight value (0-1)\r\n \"\"\"\r\n if group_name not in obj.vertex_groups:\r\n create_vertex_group(obj, group_name)\r\n \r\n group = obj.vertex_groups[group_name]\r\n group.add(vertex_indices, weight, 'REPLACE')\r\n\r\n\r\ndef set_vertex_weight(\r\n obj: bpy.types.Object,\r\n group_name: str,\r\n vertex_index: int,\r\n weight: float\r\n) -> None:\r\n \"\"\"Set weight for single vertex.\"\"\"\r\n if group_name in obj.vertex_groups:\r\n group = obj.vertex_groups[group_name]\r\n group.add([vertex_index], weight, 'REPLACE')\r\n\r\n\r\ndef get_vertex_weight(\r\n obj: bpy.types.Object,\r\n group_name: str,\r\n vertex_index: int\r\n) -> float:\r\n \"\"\"Get weight of vertex in group.\"\"\"\r\n if group_name in obj.vertex_groups:\r\n group = obj.vertex_groups[group_name]\r\n try:\r\n return group.weight(vertex_index)\r\n except RuntimeError:\r\n return 0.0\r\n return 0.0\r\n\r\n\r\ndef remove_vertices_from_group(\r\n obj: bpy.types.Object,\r\n group_name: str,\r\n vertex_indices: list\r\n) -> None:\r\n \"\"\"Remove vertices from group.\"\"\"\r\n if group_name in obj.vertex_groups:\r\n group = obj.vertex_groups[group_name]\r\n group.remove(vertex_indices)\r\n\r\n\r\ndef create_group_from_selection(\r\n obj: bpy.types.Object,\r\n group_name: str,\r\n weight: float = 1.0\r\n) -> bpy.types.VertexGroup:\r\n \"\"\"Create vertex group from selected vertices.\"\"\"\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n \r\n selected_verts = [v.index for v in obj.data.vertices if v.select]\r\n \r\n group = create_vertex_group(obj, group_name)\r\n group.add(selected_verts, weight, 'REPLACE')\r\n \r\n return group\r\n\r\n\r\ndef create_group_by_position(\r\n obj: bpy.types.Object,\r\n group_name: str,\r\n axis: str = 'Z',\r\n threshold: float = 0.0,\r\n above: bool = True,\r\n weight: float = 1.0\r\n) -> bpy.types.VertexGroup:\r\n \"\"\"\r\n Create vertex group from vertices above/below threshold on axis.\r\n \r\n Args:\r\n obj: Mesh object\r\n group_name: Group name\r\n axis: 'X', 'Y', or 'Z'\r\n threshold: Position threshold\r\n above: Select above (True) or below (False)\r\n weight: Weight value\r\n \"\"\"\r\n axis_index = {'X': 0, 'Y': 1, 'Z': 2}[axis.upper()]\r\n \r\n indices = []\r\n for v in obj.data.vertices:\r\n pos = v.co[axis_index]\r\n if (above and pos >= threshold) or (not above and pos <= threshold):\r\n indices.append(v.index)\r\n \r\n group = create_vertex_group(obj, group_name)\r\n group.add(indices, weight, 'REPLACE')\r\n \r\n return group\r\n\r\n\r\ndef create_gradient_weight(\r\n obj: bpy.types.Object,\r\n group_name: str,\r\n axis: str = 'Z',\r\n start: float = 0.0,\r\n end: float = 1.0\r\n) -> bpy.types.VertexGroup:\r\n \"\"\"\r\n Create vertex group with gradient weights along axis.\r\n \r\n Args:\r\n obj: Mesh object\r\n group_name: Group name\r\n axis: 'X', 'Y', or 'Z'\r\n start: Position for weight 0\r\n end: Position for weight 1\r\n \"\"\"\r\n axis_index = {'X': 0, 'Y': 1, 'Z': 2}[axis.upper()]\r\n \r\n group = create_vertex_group(obj, group_name)\r\n \r\n for v in obj.data.vertices:\r\n pos = v.co[axis_index]\r\n t = (pos - start) / (end - start) if end != start else 0\r\n weight = max(0, min(1, t))\r\n group.add([v.index], weight, 'REPLACE')\r\n \r\n return group\r\n\r\n\r\ndef normalize_weights(obj: bpy.types.Object) -> None:\r\n \"\"\"Normalize all vertex group weights.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n bpy.ops.object.vertex_group_normalize_all()\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef mirror_vertex_groups(obj: bpy.types.Object) -> None:\r\n \"\"\"Mirror vertex groups (L/R naming).\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.vertex_group_mirror(use_topology=False)\r\n\r\n\r\ndef remove_vertex_group(obj: bpy.types.Object, name: str) -> None:\r\n \"\"\"Remove vertex group by name.\"\"\"\r\n if name in obj.vertex_groups:\r\n obj.vertex_groups.remove(obj.vertex_groups[name])\r\n\r\n\r\ndef list_vertex_groups(obj: bpy.types.Object) -> list:\r\n \"\"\"Return list of vertex group names.\"\"\"\r\n return [g.name for g in obj.vertex_groups]","category":"utility","source_file":"vertex_group_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Normalize Weights. Category: utility","output":"import bpy\n\ndef normalize_weights(obj: bpy.types.Object) -> None:\r\n \"\"\"Normalize all vertex group weights.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n bpy.ops.object.vertex_group_normalize_all()\r\n bpy.ops.object.mode_set(mode='OBJECT')\r\n\r\n\r\ndef mirror_vertex_groups(obj: bpy.types.Object) -> None:\r\n \"\"\"Mirror vertex groups (L/R naming).\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.vertex_group_mirror(use_topology=False)\r\n\r\n\r\ndef remove_vertex_group(obj: bpy.types.Object, name: str) -> None:\r\n \"\"\"Remove vertex group by name.\"\"\"\r\n if name in obj.vertex_groups:\r\n obj.vertex_groups.remove(obj.vertex_groups[name])\r\n\r\n\r\ndef list_vertex_groups(obj: bpy.types.Object) -> list:\r\n \"\"\"Return list of vertex group names.\"\"\"\r\n return [g.name for g in obj.vertex_groups]","category":"utility","source_file":"vertex_group_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Vibrant Color Palettes for Blender Materials. Ready-to-use vibrant RGB color tuples for Blender materials. Organized by category with recommended roughness and metallic values. Use these instead of picking muted, desaturated colors.","output":"import bpy\r\n\r\n# =============================================================================\r\n# VIBRANT COLOR PALETTE REFERENCE\r\n# =============================================================================\r\n# Use these tuples directly as Base Color (R, G, B, A) values.\r\n# IMPORTANT: Always prefer saturated, vivid colors over muted/gray tones.\r\n# These are LINEAR color space values β€” Blender expects linear, not sRGB.\r\n# =============================================================================\r\n\r\n\r\n# --- NATURE COLORS ---\r\n# For outdoor scenes, landscapes, vegetation, water\r\n\r\nGRASS_GREEN = (0.08, 0.52, 0.12, 1.0) # Lush grass, roughness=0.85\r\nDARK_GRASS = (0.05, 0.35, 0.08, 1.0) # Deep forest grass, roughness=0.9\r\nFOREST_GREEN = (0.02, 0.28, 0.05, 1.0) # Pine trees, roughness=0.8\r\nLEAF_GREEN = (0.15, 0.65, 0.1, 1.0) # Fresh spring leaves, roughness=0.7\r\nOCEAN_BLUE = (0.0, 0.15, 0.65, 1.0) # Deep ocean, roughness=0.05, transmission=0.8\r\nSKY_BLUE = (0.3, 0.55, 0.9, 1.0) # Clear sky, roughness=0.5\r\nWATER_BLUE = (0.05, 0.3, 0.7, 1.0) # Rivers/lakes, roughness=0.05\r\nSUNSET_ORANGE = (0.95, 0.45, 0.05, 1.0) # Warm sunset, roughness=0.5\r\nSUNRISE_PINK = (0.9, 0.35, 0.4, 1.0) # Dawn sky, roughness=0.5\r\nEARTH_BROWN = (0.35, 0.2, 0.08, 1.0) # Soil/dirt, roughness=0.9\r\nSAND_BEIGE = (0.75, 0.62, 0.38, 1.0) # Desert sand, roughness=0.85\r\nCLAY_RED = (0.6, 0.22, 0.1, 1.0) # Red clay, roughness=0.9\r\nSNOW_WHITE = (0.95, 0.95, 0.97, 1.0) # Fresh snow, roughness=0.6\r\n\r\n\r\n# --- SPACE COLORS ---\r\n# For celestial objects, planets, sci-fi scenes\r\n\r\nSUN_YELLOW = (1.0, 0.85, 0.2, 1.0) # Star/sun emission, emission_strength=5\r\nSUN_ORANGE = (1.0, 0.6, 0.1, 1.0) # Warm star, emission_strength=5\r\nEARTH_BLUE_GREEN = (0.1, 0.45, 0.65, 1.0) # Earth from space, roughness=0.6\r\nMARS_RUST = (0.7, 0.2, 0.05, 1.0) # Mars surface, roughness=0.85\r\nMOON_GRAY = (0.45, 0.45, 0.42, 1.0) # Lunar surface, roughness=0.9\r\nJUPITER_TAN = (0.7, 0.55, 0.35, 1.0) # Gas giant, roughness=0.6\r\nSATURN_GOLD = (0.75, 0.65, 0.4, 1.0) # Saturn surface, roughness=0.6\r\nNEBULA_PURPLE = (0.5, 0.1, 0.7, 1.0) # Space nebula, emission_strength=3\r\nNEBULA_BLUE = (0.15, 0.25, 0.8, 1.0) # Blue nebula, emission_strength=2\r\nASTEROID_DARK = (0.12, 0.1, 0.08, 1.0) # Dark rock, roughness=0.95\r\nDEEP_SPACE_BG = (0.002, 0.002, 0.01, 1.0) # World background for space\r\n\r\n\r\n# --- METAL COLORS (always use metallic=1.0) ---\r\n\r\nGOLD = (1.0, 0.84, 0.0, 1.0) # Pure gold, metallic=1.0, roughness=0.2\r\nCOPPER = (0.88, 0.47, 0.3, 1.0) # Polished copper, metallic=1.0, roughness=0.25\r\nBRONZE = (0.72, 0.47, 0.15, 1.0) # Ancient bronze, metallic=1.0, roughness=0.35\r\nCHROME = (0.85, 0.85, 0.88, 1.0) # Mirror chrome, metallic=1.0, roughness=0.05\r\nIRON = (0.42, 0.42, 0.42, 1.0) # Raw iron, metallic=1.0, roughness=0.5\r\nBRUSHED_STEEL = (0.65, 0.65, 0.7, 1.0) # Brushed finish, metallic=1.0, roughness=0.4\r\nDARK_METAL = (0.18, 0.18, 0.2, 1.0) # Dark anodized, metallic=1.0, roughness=0.3\r\nROSE_GOLD = (0.9, 0.6, 0.5, 1.0) # Rose gold, metallic=1.0, roughness=0.2\r\n\r\n\r\n# --- ARCHITECTURAL / BUILDING COLORS ---\r\n\r\nSTONE_GRAY = (0.45, 0.43, 0.4, 1.0) # Castle stone, roughness=0.85\r\nDARK_STONE = (0.3, 0.28, 0.25, 1.0) # Dungeon stone, roughness=0.9\r\nBRICK_RED = (0.6, 0.18, 0.1, 1.0) # Red brick, roughness=0.85\r\nTERRACOTTA = (0.75, 0.38, 0.18, 1.0) # Roof tiles, roughness=0.8\r\nSLATE_BLUE = (0.3, 0.35, 0.42, 1.0) # Roofing slate, roughness=0.7\r\nMARBLE_WHITE = (0.9, 0.88, 0.85, 1.0) # Polished marble, roughness=0.15\r\nDARK_WOOD = (0.25, 0.13, 0.05, 1.0) # Dark timber, roughness=0.6\r\nLIGHT_OAK = (0.55, 0.38, 0.18, 1.0) # Light oak, roughness=0.55\r\nMAHOGANY = (0.35, 0.1, 0.05, 1.0) # Rich mahogany, roughness=0.5\r\n\r\n\r\n# --- VIVID / ACCENT COLORS ---\r\n\r\nPURE_RED = (0.8, 0.05, 0.02, 1.0) # Bright red, roughness=0.4\r\nPURE_BLUE = (0.05, 0.1, 0.85, 1.0) # Bright blue, roughness=0.4\r\nPURE_GREEN = (0.05, 0.7, 0.1, 1.0) # Bright green, roughness=0.4\r\nBRIGHT_YELLOW = (0.95, 0.85, 0.05, 1.0) # Vivid yellow, roughness=0.3\r\nBRIGHT_ORANGE = (0.95, 0.45, 0.02, 1.0) # Vivid orange, roughness=0.4\r\nPURPLE = (0.45, 0.05, 0.7, 1.0) # Rich purple, roughness=0.4\r\nCYAN = (0.0, 0.75, 0.8, 1.0) # Electric cyan, roughness=0.3\r\nMAGENTA = (0.8, 0.05, 0.5, 1.0) # Hot magenta, roughness=0.3\r\nCORAL = (0.9, 0.35, 0.3, 1.0) # Warm coral, roughness=0.4\r\nTEAL = (0.0, 0.55, 0.55, 1.0) # Deep teal, roughness=0.4\r\n\r\n\r\n# --- EMISSIVE COLORS (use with emission_strength 3–8) ---\r\n# IMPORTANT: For emissive materials, set BOTH Base Color AND Emission Color\r\n# to the same value. This keeps the color visible even in Material Preview.\r\n\r\nNEON_RED = (1.0, 0.1, 0.05, 1.0) # emission_strength=5\r\nNEON_BLUE = (0.1, 0.3, 1.0, 1.0) # emission_strength=5\r\nNEON_GREEN = (0.1, 1.0, 0.2, 1.0) # emission_strength=5\r\nNEON_PINK = (1.0, 0.1, 0.5, 1.0) # emission_strength=5\r\nLAVA_ORANGE = (1.0, 0.35, 0.0, 1.0) # emission_strength=6\r\nFIRE_YELLOW = (1.0, 0.7, 0.1, 1.0) # emission_strength=5\r\nICE_BLUE = (0.5, 0.8, 1.0, 1.0) # emission_strength=3\r\n\r\n\r\n# =============================================================================\r\n# UTILITY FUNCTIONS\r\n# =============================================================================\r\n\r\ndef create_vibrant_material(\r\n name: str,\r\n color: tuple,\r\n roughness: float = 0.5,\r\n metallic: float = 0.0,\r\n emission_strength: float = 0.0\r\n) -> bpy.types.Material:\r\n \"\"\"\r\n Create a material with vibrant, saturated colors.\r\n \r\n For emissive materials, this sets BOTH Base Color and Emission Color\r\n to the same value, preventing the white-washing effect in Material Preview.\r\n \r\n Args:\r\n name: Material name\r\n color: RGBA tuple like (R, G, B, 1.0) β€” use the palette constants above\r\n roughness: Surface roughness (0.0 = mirror, 1.0 = matte)\r\n metallic: Metallic value (0.0 = dielectric, 1.0 = metal)\r\n emission_strength: Glow intensity (0 = none, 3–8 recommended for visibility)\r\n \r\n Example:\r\n # Glowing sun\r\n sun_mat = create_vibrant_material(\"Sun\", SUN_YELLOW, emission_strength=5)\r\n \r\n # Metallic gold\r\n gold_mat = create_vibrant_material(\"Gold\", GOLD, roughness=0.2, metallic=1.0)\r\n \r\n # Matte grass\r\n grass_mat = create_vibrant_material(\"Grass\", GRASS_GREEN, roughness=0.85)\r\n \"\"\"\r\n mat = bpy.data.materials.new(name=name)\r\n nodes = mat.node_tree.nodes\r\n bsdf = nodes.get(\"Principled BSDF\")\r\n \r\n if bsdf:\r\n # Always set a vibrant base color\r\n bsdf.inputs['Base Color'].default_value = color if len(color) == 4 else (*color, 1.0)\r\n bsdf.inputs['Roughness'].default_value = roughness\r\n bsdf.inputs['Metallic'].default_value = metallic\r\n \r\n # For emissive materials: set BOTH base color AND emission color\r\n # This prevents the object from looking white in Material Preview\r\n if emission_strength > 0:\r\n bsdf.inputs['Emission Color'].default_value = color if len(color) == 4 else (*color, 1.0)\r\n bsdf.inputs['Emission Strength'].default_value = emission_strength\r\n \r\n return mat\r\n\r\n\r\ndef apply_vibrant_material(obj, name: str, color: tuple, **kwargs):\r\n \"\"\"Create and assign a vibrant material to an object in one call.\"\"\"\r\n mat = create_vibrant_material(name, color, **kwargs)\r\n if obj.data.materials:\r\n obj.data.materials[0] = mat\r\n else:\r\n obj.data.materials.append(mat)\r\n return mat\r\n\r\n\r\n# =============================================================================\r\n# USAGE EXAMPLES β€” Copy these patterns for vibrant materials\r\n# =============================================================================\r\n\r\n# --- Example: Vivid planet Earth ---\r\n# earth_mat = create_vibrant_material(\"Earth\", EARTH_BLUE_GREEN, roughness=0.6)\r\n# bpy.context.active_object.data.materials.append(earth_mat)\r\n\r\n# --- Example: Glowing yellow sun (NOT white) ---\r\n# sun_mat = create_vibrant_material(\"Sun\", SUN_YELLOW, emission_strength=5)\r\n# Note: emission_strength=5 keeps the yellow visible\r\n# emission_strength=15 would wash it out to white in viewport\r\n\r\n# --- Example: Polished gold ring ---\r\n# gold_mat = create_vibrant_material(\"Gold\", GOLD, roughness=0.2, metallic=1.0)\r\n\r\n# --- Example: Rusty Mars surface ---\r\n# mars_mat = create_vibrant_material(\"Mars\", MARS_RUST, roughness=0.85)","category":"utility","source_file":"vibrant_color_palettes.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Create and assign a vibrant material to an object in one call.","output":"import bpy\n\ndef apply_vibrant_material(obj, name: str, color: tuple, **kwargs):\n \"\"\"Create and assign a vibrant material to an object in one call.\"\"\"\r\n mat = create_vibrant_material(name, color, **kwargs)\r\n if obj.data.materials:\r\n obj.data.materials[0] = mat\r\n else:\r\n obj.data.materials.append(mat)\r\n return mat","category":"utility","source_file":"vibrant_color_palettes.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Viewport Utilities. Functions for controlling 3D viewport settings.","output":"import bpy\r\n\r\n\r\ndef frame_selected() -> None:\r\n \"\"\"Frame selected objects in viewport.\"\"\"\r\n bpy.ops.view3d.view_selected()\r\n\r\n\r\ndef frame_all() -> None:\r\n \"\"\"Frame all objects in viewport.\"\"\"\r\n bpy.ops.view3d.view_all()\r\n\r\n\r\ndef set_view(view: str) -> None:\r\n \"\"\"\r\n Set viewport to preset view.\r\n \r\n Args:\r\n view: 'FRONT', 'BACK', 'LEFT', 'RIGHT', 'TOP', 'BOTTOM', 'CAMERA'\r\n \"\"\"\r\n views = {\r\n 'FRONT': 'FRONT',\r\n 'BACK': 'BACK',\r\n 'LEFT': 'LEFT',\r\n 'RIGHT': 'RIGHT',\r\n 'TOP': 'TOP',\r\n 'BOTTOM': 'BOTTOM',\r\n 'CAMERA': 'CAMERA'\r\n }\r\n \r\n if view in views:\r\n bpy.ops.view3d.view_axis(type=views[view])\r\n\r\n\r\ndef toggle_orthographic() -> None:\r\n \"\"\"Toggle orthographic/perspective view.\"\"\"\r\n bpy.ops.view3d.view_persportho()\r\n\r\n\r\ndef set_shading_mode(mode: str) -> None:\r\n \"\"\"\r\n Set viewport shading mode.\r\n \r\n Args:\r\n mode: 'WIREFRAME', 'SOLID', 'MATERIAL', 'RENDERED'\r\n \"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.shading.type = mode\r\n\r\n\r\ndef set_solid_shading_options(\r\n color_type: str = 'MATERIAL',\r\n light: str = 'STUDIO',\r\n show_shadows: bool = True\r\n) -> None:\r\n \"\"\"Configure solid shading.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n shading = space.shading\r\n shading.color_type = color_type\r\n shading.light = light\r\n shading.show_shadows = show_shadows\r\n\r\n\r\ndef show_overlays(show: bool) -> None:\r\n \"\"\"Toggle viewport overlays.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.overlay.show_overlays = show\r\n\r\n\r\ndef show_grid(show: bool) -> None:\r\n \"\"\"Toggle floor grid visibility.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.overlay.show_floor = show\r\n\r\n\r\ndef show_axes(show: bool) -> None:\r\n \"\"\"Toggle axes visibility.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.overlay.show_axis_x = show\r\n space.overlay.show_axis_y = show\r\n space.overlay.show_axis_z = show\r\n\r\n\r\ndef set_clip_distances(near: float, far: float) -> None:\r\n \"\"\"Set viewport clip start/end.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.clip_start = near\r\n space.clip_end = far\r\n\r\n\r\ndef toggle_xray() -> None:\r\n \"\"\"Toggle X-Ray mode.\"\"\"\r\n bpy.context.space_data.shading.show_xray = not bpy.context.space_data.shading.show_xray\r\n\r\n\r\ndef set_focal_length(length: float) -> None:\r\n \"\"\"Set viewport camera focal length.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.lens = length","category":"utility","source_file":"viewport_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Frame Selected. Category: utility","output":"import bpy\n\ndef frame_selected() -> None:\r\n \"\"\"Frame selected objects in viewport.\"\"\"\r\n bpy.ops.view3d.view_selected()\r\n\r\n\r\ndef frame_all() -> None:\r\n \"\"\"Frame all objects in viewport.\"\"\"\r\n bpy.ops.view3d.view_all()\r\n\r\n\r\ndef set_view(view: str) -> None:\r\n \"\"\"\r\n Set viewport to preset view.\r\n \r\n Args:\r\n view: 'FRONT', 'BACK', 'LEFT', 'RIGHT', 'TOP', 'BOTTOM', 'CAMERA'\r\n \"\"\"\r\n views = {\r\n 'FRONT': 'FRONT',\r\n 'BACK': 'BACK',\r\n 'LEFT': 'LEFT',\r\n 'RIGHT': 'RIGHT',\r\n 'TOP': 'TOP',\r\n 'BOTTOM': 'BOTTOM',\r\n 'CAMERA': 'CAMERA'\r\n }\r\n \r\n if view in views:\r\n bpy.ops.view3d.view_axis(type=views[view])\r\n\r\n\r\ndef toggle_orthographic() -> None:\r\n \"\"\"Toggle orthographic/perspective view.\"\"\"\r\n bpy.ops.view3d.view_persportho()\r\n\r\n\r\ndef set_shading_mode(mode: str) -> None:\r\n \"\"\"\r\n Set viewport shading mode.\r\n \r\n Args:\r\n mode: 'WIREFRAME', 'SOLID', 'MATERIAL', 'RENDERED'\r\n \"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.shading.type = mode\r\n\r\n\r\ndef set_solid_shading_options(\r\n color_type: str = 'MATERIAL',\r\n light: str = 'STUDIO',\r\n show_shadows: bool = True\r\n) -> None:\r\n \"\"\"Configure solid shading.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n shading = space.shading\r\n shading.color_type = color_type\r\n shading.light = light\r\n shading.show_shadows = show_shadows\r\n\r\n\r\ndef show_overlays(show: bool) -> None:\r\n \"\"\"Toggle viewport overlays.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.overlay.show_overlays = show\r\n\r\n\r\ndef show_grid(show: bool) -> None:\r\n \"\"\"Toggle floor grid visibility.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.overlay.show_floor = show\r\n\r\n\r\ndef show_axes(show: bool) -> None:\r\n \"\"\"Toggle axes visibility.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.overlay.show_axis_x = show\r\n space.overlay.show_axis_y = show\r\n space.overlay.show_axis_z = show\r\n\r\n\r\ndef set_clip_distances(near: float, far: float) -> None:\r\n \"\"\"Set viewport clip start/end.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.clip_start = near\r\n space.clip_end = far\r\n\r\n\r\ndef toggle_xray() -> None:\r\n \"\"\"Toggle X-Ray mode.\"\"\"\r\n bpy.context.space_data.shading.show_xray = not bpy.context.space_data.shading.show_xray\r\n\r\n\r\ndef set_focal_length(length: float) -> None:\r\n \"\"\"Set viewport camera focal length.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.lens = length","category":"utility","source_file":"viewport_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Volumetric Effects β€” Atmosphere, God Rays, Fog. Create atmospheric depth using volumetric shaders. Key principle: Principled Volume β†’ Volume output (NOT Surface output). DENSITY GUIDELINES:","output":"import bpy\r\n\r\n\r\n# =============================================================================\r\n# VOLUMETRIC ATMOSPHERE β€” God Rays & Depth\r\n# =============================================================================\r\n\r\ndef create_atmosphere(density=0.005, scale=10):\r\n \"\"\"Create atmospheric volume for god rays and depth.\r\n\r\n Creates a large cube with Principled Volume material.\r\n CRITICAL: Volume shader connects to Volume output (not Surface).\r\n\r\n Args:\r\n density: Volumetric density (0.001=clear, 0.005=haze, 0.05=fog)\r\n scale: Size of the atmosphere cube (should cover entire scene)\r\n \"\"\"\r\n # Create domain cube\r\n bpy.ops.mesh.primitive_cube_add(size=1)\r\n cube = bpy.context.active_object\r\n cube.name = \"Atmosphere_Volume\"\r\n cube.scale = (scale, scale, scale)\r\n cube.display_type = 'WIRE' # Don't obstruct viewport\r\n\r\n # Create Volume Material\r\n mat = bpy.data.materials.new(name=\"Volumetric_Atmosphere\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Principled Volume node\r\n vol_node = nodes.new('ShaderNodeVolumePrincipled')\r\n vol_node.location = (0, 0)\r\n vol_node.inputs['Density'].default_value = density\r\n vol_node.inputs['Color'].default_value = (0.8, 0.85, 0.9) # Slightly blue haze\r\n\r\n # Output node\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (300, 0)\r\n\r\n # CRITICAL: Connect to VOLUME output, NOT Surface\r\n links.new(vol_node.outputs['Volume'], output.inputs['Volume'])\r\n\r\n cube.data.materials.append(mat)\r\n return cube\r\n\r\n\r\n# =============================================================================\r\n# FOG β€” Ground-Level Atmospheric Effect\r\n# =============================================================================\r\n\r\ndef create_ground_fog(density=0.03, height=2.0, scale=20):\r\n \"\"\"Create ground-hugging fog using a flattened volume domain.\r\n\r\n Args:\r\n density: Fog density (0.02-0.1)\r\n height: Height of fog layer in meters\r\n scale: Horizontal coverage\r\n \"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1)\r\n fog = bpy.context.active_object\r\n fog.name = \"Ground_Fog\"\r\n fog.scale = (scale, scale, height / 2)\r\n fog.location = (0, 0, height / 2)\r\n fog.display_type = 'WIRE'\r\n\r\n mat = bpy.data.materials.new(name=\"Fog_Material\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Volume with gradient for natural falloff\r\n vol_node = nodes.new('ShaderNodeVolumePrincipled')\r\n vol_node.location = (0, 0)\r\n vol_node.inputs['Density'].default_value = density\r\n vol_node.inputs['Color'].default_value = (0.9, 0.9, 0.95)\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (300, 0)\r\n\r\n # CRITICAL: Volume output, not Surface\r\n links.new(vol_node.outputs['Volume'], output.inputs['Volume'])\r\n\r\n fog.data.materials.append(mat)\r\n return fog\r\n\r\n\r\n# =============================================================================\r\n# PROCEDURAL FOG WITH NOISE β€” More Realistic\r\n# =============================================================================\r\n\r\ndef create_procedural_fog(density=0.02, noise_scale=3.0, height=3.0, coverage=20):\r\n \"\"\"Create fog with procedural noise for organic wispy appearance.\r\n\r\n Uses Noise Texture to vary density across the volume.\r\n \"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1)\r\n fog = bpy.context.active_object\r\n fog.name = \"Procedural_Fog\"\r\n fog.scale = (coverage, coverage, height)\r\n fog.location = (0, 0, height)\r\n fog.display_type = 'WIRE'\r\n\r\n mat = bpy.data.materials.new(name=\"Procedural_Fog_Mat\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Texture Coordinate β†’ Mapping β†’ Noise β†’ Multiply with Density\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-600, 0)\r\n\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, 0)\r\n noise.inputs['Scale'].default_value = noise_scale\r\n noise.inputs['Detail'].default_value = 5.0\r\n\r\n # Math multiply to control density\r\n multiply = nodes.new('ShaderNodeMath')\r\n multiply.location = (-200, 0)\r\n multiply.operation = 'MULTIPLY'\r\n multiply.inputs[1].default_value = density\r\n\r\n # Principled Volume\r\n vol_node = nodes.new('ShaderNodeVolumePrincipled')\r\n vol_node.location = (0, 0)\r\n vol_node.inputs['Color'].default_value = (0.85, 0.88, 0.92)\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (300, 0)\r\n\r\n # Link chain\r\n links.new(tex_coord.outputs['Object'], noise.inputs['Vector'])\r\n links.new(noise.outputs['Fac'], multiply.inputs[0])\r\n links.new(multiply.outputs['Value'], vol_node.inputs['Density'])\r\n links.new(vol_node.outputs['Volume'], output.inputs['Volume'])\r\n\r\n fog.data.materials.append(mat)\r\n return fog\r\n\r\n\r\n# =============================================================================\r\n# WORLD VOLUME β€” Entire Scene Atmosphere\r\n# =============================================================================\r\n\r\ndef create_world_volume(density=0.002):\r\n \"\"\"Add volumetric atmosphere via World shader (affects entire scene).\r\n\r\n Simpler than domain cube but less controllable.\r\n Good for subtle overall haze.\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if not world:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n world.use_nodes = True\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n\r\n # Find or create output\r\n output = None\r\n for node in nodes:\r\n if node.type == 'OUTPUT_WORLD':\r\n output = node\r\n break\r\n if not output:\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n\r\n # Create volume shader\r\n vol_node = nodes.new('ShaderNodeVolumePrincipled')\r\n vol_node.location = (-200, -200)\r\n vol_node.inputs['Density'].default_value = density\r\n vol_node.inputs['Color'].default_value = (0.8, 0.85, 0.92)\r\n\r\n # Connect to Volume output of World\r\n links.new(vol_node.outputs['Volume'], output.inputs['Volume'])\r\n\r\n return world\r\n\r\n\r\n# =============================================================================\r\n# HDRI WITH VOLUMETRIC ATMOSPHERE β€” Complete Lighting Setup\r\n# =============================================================================\r\n\r\ndef setup_hdri_with_atmosphere(hdri_path, strength=1.0, atmo_density=0.003):\r\n \"\"\"Complete setup: HDRI environment lighting + volumetric atmosphere.\r\n\r\n Args:\r\n hdri_path: Path to .hdr or .exr file\r\n strength: HDRI intensity\r\n atmo_density: Atmospheric density for god rays\r\n \"\"\"\r\n # Set up HDRI world\r\n world = bpy.context.scene.world\r\n if not world:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n world.use_nodes = True\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n\r\n # Environment Texture\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.location = (-400, 200)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n\r\n # Mapping for rotation\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 200)\r\n\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-600, 200)\r\n\r\n # Background\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.location = (-200, 200)\r\n bg.inputs['Strength'].default_value = strength\r\n\r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n\r\n # Link: Coord β†’ Mapping β†’ Env Tex β†’ Background β†’ Output\r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n\r\n # Add atmosphere domain for god rays\r\n atmo = create_atmosphere(density=atmo_density)\r\n\r\n return world, atmo","category":"utility","source_file":"volumetric_effects.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function to: Create atmospheric volume for god rays and depth.\r\n\r\n Creates a large cube with Principled Volume material.\r\n CRITICAL: Volume shader connects to Volume output (not Surface).\r\n\r\n Args:\r\n density: Volumetric density (0.001=clear, 0.005=haze, 0.05=fog)\r\n scale: Size of the atmosphere cube (should cover entire scene)","output":"import bpy\n\ndef create_atmosphere(density=0.005, scale=10):\n \"\"\"Create atmospheric volume for god rays and depth.\r\n\r\n Creates a large cube with Principled Volume material.\r\n CRITICAL: Volume shader connects to Volume output (not Surface).\r\n\r\n Args:\r\n density: Volumetric density (0.001=clear, 0.005=haze, 0.05=fog)\r\n scale: Size of the atmosphere cube (should cover entire scene)\r\n \"\"\"\r\n # Create domain cube\r\n bpy.ops.mesh.primitive_cube_add(size=1)\r\n cube = bpy.context.active_object\r\n cube.name = \"Atmosphere_Volume\"\r\n cube.scale = (scale, scale, scale)\r\n cube.display_type = 'WIRE' # Don't obstruct viewport\r\n\r\n # Create Volume Material\r\n mat = bpy.data.materials.new(name=\"Volumetric_Atmosphere\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Principled Volume node\r\n vol_node = nodes.new('ShaderNodeVolumePrincipled')\r\n vol_node.location = (0, 0)\r\n vol_node.inputs['Density'].default_value = density\r\n vol_node.inputs['Color'].default_value = (0.8, 0.85, 0.9) # Slightly blue haze\r\n\r\n # Output node\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (300, 0)\r\n\r\n # CRITICAL: Connect to VOLUME output, NOT Surface\r\n links.new(vol_node.outputs['Volume'], output.inputs['Volume'])\r\n\r\n cube.data.materials.append(mat)\r\n return cube","category":"utility","source_file":"volumetric_effects.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create ground-hugging fog using a flattened volume domain.\r\n\r\n Args:\r\n density: Fog density (0.02-0.1)\r\n height: Height of fog layer in meters\r\n scale: Horizontal coverage","output":"import bpy\n\ndef create_ground_fog(density=0.03, height=2.0, scale=20):\n \"\"\"Create ground-hugging fog using a flattened volume domain.\r\n\r\n Args:\r\n density: Fog density (0.02-0.1)\r\n height: Height of fog layer in meters\r\n scale: Horizontal coverage\r\n \"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1)\r\n fog = bpy.context.active_object\r\n fog.name = \"Ground_Fog\"\r\n fog.scale = (scale, scale, height / 2)\r\n fog.location = (0, 0, height / 2)\r\n fog.display_type = 'WIRE'\r\n\r\n mat = bpy.data.materials.new(name=\"Fog_Material\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Volume with gradient for natural falloff\r\n vol_node = nodes.new('ShaderNodeVolumePrincipled')\r\n vol_node.location = (0, 0)\r\n vol_node.inputs['Density'].default_value = density\r\n vol_node.inputs['Color'].default_value = (0.9, 0.9, 0.95)\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (300, 0)\r\n\r\n # CRITICAL: Volume output, not Surface\r\n links.new(vol_node.outputs['Volume'], output.inputs['Volume'])\r\n\r\n fog.data.materials.append(mat)\r\n return fog","category":"utility","source_file":"volumetric_effects.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Create fog with procedural noise for organic wispy appearance.\r\n\r\n Uses Noise Texture to vary density across the volume.","output":"import bpy\n\ndef create_procedural_fog(density=0.02, noise_scale=3.0, height=3.0, coverage=20):\n \"\"\"Create fog with procedural noise for organic wispy appearance.\r\n\r\n Uses Noise Texture to vary density across the volume.\r\n \"\"\"\r\n bpy.ops.mesh.primitive_cube_add(size=1)\r\n fog = bpy.context.active_object\r\n fog.name = \"Procedural_Fog\"\r\n fog.scale = (coverage, coverage, height)\r\n fog.location = (0, 0, height)\r\n fog.display_type = 'WIRE'\r\n\r\n mat = bpy.data.materials.new(name=\"Procedural_Fog_Mat\")\r\n nodes = mat.node_tree.nodes\r\n links = mat.node_tree.links\r\n nodes.clear()\r\n\r\n # Texture Coordinate β†’ Mapping β†’ Noise β†’ Multiply with Density\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-600, 0)\r\n\r\n noise = nodes.new('ShaderNodeTexNoise')\r\n noise.location = (-400, 0)\r\n noise.inputs['Scale'].default_value = noise_scale\r\n noise.inputs['Detail'].default_value = 5.0\r\n\r\n # Math multiply to control density\r\n multiply = nodes.new('ShaderNodeMath')\r\n multiply.location = (-200, 0)\r\n multiply.operation = 'MULTIPLY'\r\n multiply.inputs[1].default_value = density\r\n\r\n # Principled Volume\r\n vol_node = nodes.new('ShaderNodeVolumePrincipled')\r\n vol_node.location = (0, 0)\r\n vol_node.inputs['Color'].default_value = (0.85, 0.88, 0.92)\r\n\r\n output = nodes.new('ShaderNodeOutputMaterial')\r\n output.location = (300, 0)\r\n\r\n # Link chain\r\n links.new(tex_coord.outputs['Object'], noise.inputs['Vector'])\r\n links.new(noise.outputs['Fac'], multiply.inputs[0])\r\n links.new(multiply.outputs['Value'], vol_node.inputs['Density'])\r\n links.new(vol_node.outputs['Volume'], output.inputs['Volume'])\r\n\r\n fog.data.materials.append(mat)\r\n return fog","category":"utility","source_file":"volumetric_effects.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Add volumetric atmosphere via World shader (affects entire scene).\r\n\r\n Simpler than domain cube but less controllable.\r\n Good for subtle overall haze.","output":"import bpy\n\ndef create_world_volume(density=0.002):\n \"\"\"Add volumetric atmosphere via World shader (affects entire scene).\r\n\r\n Simpler than domain cube but less controllable.\r\n Good for subtle overall haze.\r\n \"\"\"\r\n world = bpy.context.scene.world\r\n if not world:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n world.use_nodes = True\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n\r\n # Find or create output\r\n output = None\r\n for node in nodes:\r\n if node.type == 'OUTPUT_WORLD':\r\n output = node\r\n break\r\n if not output:\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n\r\n # Create volume shader\r\n vol_node = nodes.new('ShaderNodeVolumePrincipled')\r\n vol_node.location = (-200, -200)\r\n vol_node.inputs['Density'].default_value = density\r\n vol_node.inputs['Color'].default_value = (0.8, 0.85, 0.92)\r\n\r\n # Connect to Volume output of World\r\n links.new(vol_node.outputs['Volume'], output.inputs['Volume'])\r\n\r\n return world","category":"utility","source_file":"volumetric_effects.py","pair_type":"function"} +{"instruction":"Write a Blender Python function to: Complete setup: HDRI environment lighting + volumetric atmosphere.\r\n\r\n Args:\r\n hdri_path: Path to .hdr or .exr file\r\n strength: HDRI intensity\r\n atmo_density: Atmospheric density for god rays","output":"import bpy\n\ndef setup_hdri_with_atmosphere(hdri_path, strength=1.0, atmo_density=0.003):\n \"\"\"Complete setup: HDRI environment lighting + volumetric atmosphere.\r\n\r\n Args:\r\n hdri_path: Path to .hdr or .exr file\r\n strength: HDRI intensity\r\n atmo_density: Atmospheric density for god rays\r\n \"\"\"\r\n # Set up HDRI world\r\n world = bpy.context.scene.world\r\n if not world:\r\n world = bpy.data.worlds.new(\"World\")\r\n bpy.context.scene.world = world\r\n\r\n world.use_nodes = True\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n nodes.clear()\r\n\r\n # Environment Texture\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.location = (-400, 200)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n\r\n # Mapping for rotation\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-800, 200)\r\n\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-600, 200)\r\n\r\n # Background\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.location = (-200, 200)\r\n bg.inputs['Strength'].default_value = strength\r\n\r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n\r\n # Link: Coord β†’ Mapping β†’ Env Tex β†’ Background β†’ Output\r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n\r\n # Add atmosphere domain for god rays\r\n atmo = create_atmosphere(density=atmo_density)\r\n\r\n return world, atmo","category":"utility","source_file":"volumetric_effects.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: Weight Paint Utilities. Functions for weight painting and vertex weight management.","output":"import bpy\r\n\r\n\r\ndef enable_weight_paint_mode(obj: bpy.types.Object = None) -> None:\r\n \"\"\"Enter weight paint mode.\"\"\"\r\n if obj:\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n\r\n\r\ndef set_active_vertex_group(\r\n obj: bpy.types.Object,\r\n group_name: str\r\n) -> int:\r\n \"\"\"\r\n Set active vertex group for painting.\r\n \r\n Args:\r\n obj: Target object\r\n group_name: Vertex group name\r\n \r\n Returns:\r\n Index of the active group\r\n \"\"\"\r\n index = obj.vertex_groups.find(group_name)\r\n if index >= 0:\r\n obj.vertex_groups.active_index = index\r\n return index\r\n\r\n\r\ndef set_weight(weight: float = 1.0) -> None:\r\n \"\"\"Set brush weight value (0-1).\"\"\"\r\n ts = bpy.context.tool_settings\r\n ts.unified_paint_settings.weight = weight\r\n\r\n\r\ndef set_brush_strength(strength: float = 1.0) -> None:\r\n \"\"\"Set brush strength.\"\"\"\r\n brush = bpy.context.tool_settings.weight_paint.brush\r\n if brush:\r\n brush.strength = strength\r\n\r\n\r\ndef set_brush_radius(radius: int = 50) -> None:\r\n \"\"\"Set brush radius in pixels.\"\"\"\r\n brush = bpy.context.tool_settings.weight_paint.brush\r\n if brush:\r\n brush.size = radius\r\n\r\n\r\ndef set_weight_brush(brush_type: str = 'Draw') -> None:\r\n \"\"\"\r\n Set weight paint brush.\r\n \r\n Args:\r\n brush_type: 'Draw', 'Blur', 'Average', 'Smear'\r\n \"\"\"\r\n brush = bpy.data.brushes.get(brush_type)\r\n if brush:\r\n bpy.context.tool_settings.weight_paint.brush = brush\r\n\r\n\r\ndef normalize_all_weights(\r\n obj: bpy.types.Object,\r\n lock_active: bool = False\r\n) -> None:\r\n \"\"\"Normalize all vertex groups.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n bpy.ops.object.vertex_group_normalize_all(lock_active=lock_active)\r\n\r\n\r\ndef clean_weights(\r\n obj: bpy.types.Object,\r\n threshold: float = 0.01\r\n) -> None:\r\n \"\"\"\r\n Remove weights below threshold.\r\n \r\n Args:\r\n obj: Target object\r\n threshold: Minimum weight to keep\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n \r\n for vg in obj.vertex_groups:\r\n obj.vertex_groups.active_index = vg.index\r\n bpy.ops.object.vertex_group_clean(group_select_mode='ALL', limit=threshold)\r\n\r\n\r\ndef smooth_weights(\r\n obj: bpy.types.Object,\r\n factor: float = 0.5,\r\n iterations: int = 1\r\n) -> None:\r\n \"\"\"Smooth vertex weights.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n \r\n for _ in range(iterations):\r\n bpy.ops.object.vertex_group_smooth(\r\n factor=factor,\r\n repeat=1\r\n )\r\n\r\n\r\ndef limit_total_weights(\r\n obj: bpy.types.Object,\r\n limit: int = 4\r\n) -> None:\r\n \"\"\"Limit total influences per vertex.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n bpy.ops.object.vertex_group_limit_total(limit=limit)\r\n\r\n\r\ndef quantize_weights(\r\n obj: bpy.types.Object,\r\n steps: int = 4\r\n) -> None:\r\n \"\"\"Quantize weights to discrete steps.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n bpy.ops.object.vertex_group_quantize(group_select_mode='ALL', steps=steps)\r\n\r\n\r\ndef mirror_weights(\r\n obj: bpy.types.Object,\r\n direction: str = 'POSITIVE_X'\r\n) -> None:\r\n \"\"\"Mirror weights across axis.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n bpy.ops.object.vertex_group_mirror(\r\n mirror_weights=True,\r\n use_topology=False\r\n )\r\n\r\n\r\ndef transfer_weights(\r\n source: bpy.types.Object,\r\n target: bpy.types.Object,\r\n method: str = 'NEAREST_FACE'\r\n) -> None:\r\n \"\"\"\r\n Transfer weights from source to target.\r\n \r\n Args:\r\n source: Source mesh with weights\r\n target: Target mesh\r\n method: 'NEAREST', 'NEAREST_FACE', 'PROJECTED'\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = target\r\n target.select_set(True)\r\n source.select_set(True)\r\n \r\n bpy.ops.object.data_transfer(\r\n use_reverse_transfer=False,\r\n data_type='VGROUP_WEIGHTS',\r\n vert_mapping=method,\r\n layers_select_src='ALL',\r\n layers_select_dst='NAME'\r\n )\r\n\r\n\r\ndef auto_weights_from_bones(\r\n mesh: bpy.types.Object,\r\n armature: bpy.types.Object\r\n) -> None:\r\n \"\"\"\r\n Auto-generate weights from armature bones.\r\n \r\n Args:\r\n mesh: Target mesh\r\n armature: Armature object\r\n \"\"\"\r\n bpy.ops.object.select_all(action='DESELECT')\r\n mesh.select_set(True)\r\n armature.select_set(True)\r\n bpy.context.view_layer.objects.active = armature\r\n \r\n bpy.ops.object.parent_set(type='ARMATURE_AUTO')\r\n\r\n\r\ndef show_weight_overlay(show: bool = True) -> None:\r\n \"\"\"Toggle weight visualization overlay.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.overlay.show_weight = show","category":"utility","source_file":"weight_paint_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Enable Weight Paint Mode. Category: utility","output":"import bpy\n\ndef enable_weight_paint_mode(obj: bpy.types.Object = None) -> None:\r\n \"\"\"Enter weight paint mode.\"\"\"\r\n if obj:\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n\r\n\r\ndef set_active_vertex_group(\r\n obj: bpy.types.Object,\r\n group_name: str\r\n) -> int:\r\n \"\"\"\r\n Set active vertex group for painting.\r\n \r\n Args:\r\n obj: Target object\r\n group_name: Vertex group name\r\n \r\n Returns:\r\n Index of the active group\r\n \"\"\"\r\n index = obj.vertex_groups.find(group_name)\r\n if index >= 0:\r\n obj.vertex_groups.active_index = index\r\n return index\r\n\r\n\r\ndef set_weight(weight: float = 1.0) -> None:\r\n \"\"\"Set brush weight value (0-1).\"\"\"\r\n ts = bpy.context.tool_settings\r\n ts.unified_paint_settings.weight = weight\r\n\r\n\r\ndef set_brush_strength(strength: float = 1.0) -> None:\r\n \"\"\"Set brush strength.\"\"\"\r\n brush = bpy.context.tool_settings.weight_paint.brush\r\n if brush:\r\n brush.strength = strength\r\n\r\n\r\ndef set_brush_radius(radius: int = 50) -> None:\r\n \"\"\"Set brush radius in pixels.\"\"\"\r\n brush = bpy.context.tool_settings.weight_paint.brush\r\n if brush:\r\n brush.size = radius\r\n\r\n\r\ndef set_weight_brush(brush_type: str = 'Draw') -> None:\r\n \"\"\"\r\n Set weight paint brush.\r\n \r\n Args:\r\n brush_type: 'Draw', 'Blur', 'Average', 'Smear'\r\n \"\"\"\r\n brush = bpy.data.brushes.get(brush_type)\r\n if brush:\r\n bpy.context.tool_settings.weight_paint.brush = brush\r\n\r\n\r\ndef normalize_all_weights(\r\n obj: bpy.types.Object,\r\n lock_active: bool = False\r\n) -> None:\r\n \"\"\"Normalize all vertex groups.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n bpy.ops.object.vertex_group_normalize_all(lock_active=lock_active)\r\n\r\n\r\ndef clean_weights(\r\n obj: bpy.types.Object,\r\n threshold: float = 0.01\r\n) -> None:\r\n \"\"\"\r\n Remove weights below threshold.\r\n \r\n Args:\r\n obj: Target object\r\n threshold: Minimum weight to keep\r\n \"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n \r\n for vg in obj.vertex_groups:\r\n obj.vertex_groups.active_index = vg.index\r\n bpy.ops.object.vertex_group_clean(group_select_mode='ALL', limit=threshold)\r\n\r\n\r\ndef smooth_weights(\r\n obj: bpy.types.Object,\r\n factor: float = 0.5,\r\n iterations: int = 1\r\n) -> None:\r\n \"\"\"Smooth vertex weights.\"\"\"\r\n bpy.context.view_layer.objects.active = obj\r\n bpy.ops.object.mode_set(mode='WEIGHT_PAINT')\r\n \r\n for _ in range(iterations):\n bpy.ops.object.vertex_group_smooth(\r\n factor=factor,\r\n repeat=1\r\n )","category":"utility","source_file":"weight_paint_utils.py","pair_type":"function"} +{"instruction":"Write a Blender Python function: Show Weight Overlay. Category: utility","output":"import bpy\n\ndef show_weight_overlay(show: bool = True) -> None:\r\n \"\"\"Toggle weight visualization overlay.\"\"\"\r\n for area in bpy.context.screen.areas:\r\n if area.type == 'VIEW_3D':\r\n for space in area.spaces:\r\n if space.type == 'VIEW_3D':\r\n space.overlay.show_weight = show","category":"utility","source_file":"weight_paint_utils.py","pair_type":"function"} +{"instruction":"Write a complete Blender Python script: World Settings Utilities. Functions for configuring world/environment settings.","output":"import bpy\r\nimport math\r\n\r\n\r\ndef set_background_color(color: tuple = (0.05, 0.05, 0.05)) -> None:\r\n \"\"\"Set solid background color.\"\"\"\r\n world = _ensure_world()\r\n nodes = world.node_tree.nodes\r\n \r\n bg = nodes.get('Background')\r\n if bg:\r\n bg.inputs['Color'].default_value = (*color, 1.0)\r\n bg.inputs['Strength'].default_value = 1.0\r\n\r\n\r\ndef set_hdri_background(\r\n hdri_path: str,\r\n strength: float = 1.0,\r\n rotation: float = 0\r\n) -> None:\r\n \"\"\"\r\n Set HDRI environment map.\r\n \r\n Args:\r\n hdri_path: Path to HDRI image\r\n strength: Environment strength\r\n rotation: Z rotation in degrees\r\n \"\"\"\r\n world = _ensure_world()\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Texture coordinate\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-600, 0)\r\n \r\n # Mapping for rotation\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-400, 0)\r\n mapping.inputs['Rotation'].default_value[2] = math.radians(rotation)\r\n \r\n # Environment texture\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.location = (-200, 0)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n \r\n # Background\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.location = (0, 0)\r\n bg.inputs['Strength'].default_value = strength\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n \r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef set_sky_texture(\r\n sun_elevation: float = 45,\r\n sun_rotation: float = 0,\r\n turbidity: float = 2.2,\r\n strength: float = 1.0\r\n) -> None:\r\n \"\"\"\r\n Set procedural sky texture.\r\n \r\n Args:\r\n sun_elevation: Sun angle above horizon\r\n sun_rotation: Sun horizontal rotation\r\n turbidity: Atmospheric haze\r\n strength: Sky brightness\r\n \"\"\"\r\n world = _ensure_world()\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Sky texture\r\n sky = nodes.new('ShaderNodeTexSky')\r\n sky.location = (-200, 0)\r\n sky.sky_type = 'NISHITA'\r\n sky.sun_elevation = math.radians(sun_elevation)\r\n sky.sun_rotation = math.radians(sun_rotation)\r\n sky.air_density = turbidity\r\n \r\n # Background\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.location = (0, 0)\r\n bg.inputs['Strength'].default_value = strength\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n \r\n links.new(sky.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef set_gradient_background(\r\n top_color: tuple = (0.05, 0.1, 0.2),\r\n bottom_color: tuple = (0.4, 0.35, 0.3),\r\n blend_type: str = 'LINEAR'\r\n) -> None:\r\n \"\"\"\r\n Set gradient background.\r\n \r\n Args:\r\n top_color: RGB color at top\r\n bottom_color: RGB color at bottom\r\n blend_type: 'LINEAR', 'QUADRATIC', 'EASING'\r\n \"\"\"\r\n world = _ensure_world()\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Texture coordinate\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-400, 0)\r\n \r\n # Separate XYZ to get Z\r\n separate = nodes.new('ShaderNodeSeparateXYZ')\r\n separate.location = (-200, 0)\r\n \r\n # Color ramp\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (0, 0)\r\n ramp.color_ramp.elements[0].color = (*bottom_color, 1.0)\r\n ramp.color_ramp.elements[1].color = (*top_color, 1.0)\r\n \r\n if blend_type == 'QUADRATIC':\r\n ramp.color_ramp.interpolation = 'EASE'\r\n \r\n # Background\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.location = (200, 0)\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (400, 0)\r\n \r\n links.new(tex_coord.outputs['Generated'], separate.inputs['Vector'])\r\n links.new(separate.outputs['Z'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef _ensure_world() -> bpy.types.World:\r\n \"\"\"Ensure scene has a world.\"\"\"\r\n if not bpy.context.scene.world:\r\n bpy.context.scene.world = bpy.data.worlds.new(\"World\")\r\n return bpy.context.scene.world\r\n\r\n\r\ndef set_ambient_occlusion(\r\n enabled: bool = True,\r\n distance: float = 1.0,\r\n factor: float = 1.0\r\n) -> None:\r\n \"\"\"Configure world ambient occlusion.\"\"\"\r\n world = _ensure_world()\r\n world.light_settings.use_ambient_occlusion = enabled\r\n world.light_settings.ao_factor = factor\r\n world.light_settings.distance = distance\r\n\r\n\r\nif __name__ == \"__main__\":\r\n set_sky_texture(sun_elevation=30)\r\n print(\"Set sky texture\")","category":"utility","source_file":"world_utils.py","pair_type":"full_script"} +{"instruction":"Write a Blender Python function: Set Background Color. Category: utility","output":"import bpy\n\ndef set_background_color(color: tuple = (0.05, 0.05, 0.05)) -> None:\r\n \"\"\"Set solid background color.\"\"\"\r\n world = _ensure_world()\r\n nodes = world.node_tree.nodes\r\n \r\n bg = nodes.get('Background')\r\n if bg:\r\n bg.inputs['Color'].default_value = (*color, 1.0)\r\n bg.inputs['Strength'].default_value = 1.0\r\n\r\n\r\ndef set_hdri_background(\r\n hdri_path: str,\r\n strength: float = 1.0,\r\n rotation: float = 0\r\n) -> None:\r\n \"\"\"\r\n Set HDRI environment map.\r\n \r\n Args:\r\n hdri_path: Path to HDRI image\r\n strength: Environment strength\r\n rotation: Z rotation in degrees\r\n \"\"\"\r\n world = _ensure_world()\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Texture coordinate\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-600, 0)\r\n \r\n # Mapping for rotation\r\n mapping = nodes.new('ShaderNodeMapping')\r\n mapping.location = (-400, 0)\r\n mapping.inputs['Rotation'].default_value[2] = math.radians(rotation)\r\n \r\n # Environment texture\r\n env_tex = nodes.new('ShaderNodeTexEnvironment')\r\n env_tex.location = (-200, 0)\r\n env_tex.image = bpy.data.images.load(hdri_path)\r\n \r\n # Background\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.location = (0, 0)\r\n bg.inputs['Strength'].default_value = strength\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n \r\n links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])\r\n links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])\r\n links.new(env_tex.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef set_sky_texture(\r\n sun_elevation: float = 45,\r\n sun_rotation: float = 0,\r\n turbidity: float = 2.2,\r\n strength: float = 1.0\r\n) -> None:\r\n \"\"\"\r\n Set procedural sky texture.\r\n \r\n Args:\r\n sun_elevation: Sun angle above horizon\r\n sun_rotation: Sun horizontal rotation\r\n turbidity: Atmospheric haze\r\n strength: Sky brightness\r\n \"\"\"\r\n world = _ensure_world()\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Sky texture\r\n sky = nodes.new('ShaderNodeTexSky')\r\n sky.location = (-200, 0)\r\n sky.sky_type = 'NISHITA'\r\n sky.sun_elevation = math.radians(sun_elevation)\r\n sky.sun_rotation = math.radians(sun_rotation)\r\n sky.air_density = turbidity\r\n \r\n # Background\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.location = (0, 0)\r\n bg.inputs['Strength'].default_value = strength\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (200, 0)\r\n \r\n links.new(sky.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef set_gradient_background(\r\n top_color: tuple = (0.05, 0.1, 0.2),\r\n bottom_color: tuple = (0.4, 0.35, 0.3),\r\n blend_type: str = 'LINEAR'\r\n) -> None:\r\n \"\"\"\r\n Set gradient background.\r\n \r\n Args:\r\n top_color: RGB color at top\r\n bottom_color: RGB color at bottom\r\n blend_type: 'LINEAR', 'QUADRATIC', 'EASING'\r\n \"\"\"\r\n world = _ensure_world()\r\n nodes = world.node_tree.nodes\r\n links = world.node_tree.links\r\n \r\n nodes.clear()\r\n \r\n # Texture coordinate\r\n tex_coord = nodes.new('ShaderNodeTexCoord')\r\n tex_coord.location = (-400, 0)\r\n \r\n # Separate XYZ to get Z\r\n separate = nodes.new('ShaderNodeSeparateXYZ')\r\n separate.location = (-200, 0)\r\n \r\n # Color ramp\r\n ramp = nodes.new('ShaderNodeValToRGB')\r\n ramp.location = (0, 0)\r\n ramp.color_ramp.elements[0].color = (*bottom_color, 1.0)\r\n ramp.color_ramp.elements[1].color = (*top_color, 1.0)\r\n \r\n if blend_type == 'QUADRATIC':\r\n ramp.color_ramp.interpolation = 'EASE'\r\n \r\n # Background\r\n bg = nodes.new('ShaderNodeBackground')\r\n bg.location = (200, 0)\r\n \r\n # Output\r\n output = nodes.new('ShaderNodeOutputWorld')\r\n output.location = (400, 0)\r\n \r\n links.new(tex_coord.outputs['Generated'], separate.inputs['Vector'])\r\n links.new(separate.outputs['Z'], ramp.inputs['Fac'])\r\n links.new(ramp.outputs['Color'], bg.inputs['Color'])\r\n links.new(bg.outputs['Background'], output.inputs['Surface'])\r\n\r\n\r\ndef _ensure_world() -> bpy.types.World:\r\n \"\"\"Ensure scene has a world.\"\"\"\r\n if not bpy.context.scene.world:\r\n bpy.context.scene.world = bpy.data.worlds.new(\"World\")\r\n return bpy.context.scene.world\r\n\r\n\r\ndef set_ambient_occlusion(\r\n enabled: bool = True,\r\n distance: float = 1.0,\r\n factor: float = 1.0\r\n) -> None:\r\n \"\"\"Configure world ambient occlusion.\"\"\"\r\n world = _ensure_world()\r\n world.light_settings.use_ambient_occlusion = enabled\r\n world.light_settings.ao_factor = factor\r\n world.light_settings.distance = distance\r\n\r\n\r\nif __name__ == \"__main__\":\r\n set_sky_texture(sun_elevation=30)\r\n print(\"Set sky texture\")","category":"utility","source_file":"world_utils.py","pair_type":"function"}