amd-gaia 0.15.0__py3-none-any.whl → 0.15.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/METADATA +222 -223
- amd_gaia-0.15.2.dist-info/RECORD +182 -0
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/WHEEL +1 -1
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/entry_points.txt +1 -0
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/licenses/LICENSE.md +20 -20
- gaia/__init__.py +29 -29
- gaia/agents/__init__.py +19 -19
- gaia/agents/base/__init__.py +9 -9
- gaia/agents/base/agent.py +2132 -2177
- gaia/agents/base/api_agent.py +119 -120
- gaia/agents/base/console.py +1967 -1841
- gaia/agents/base/errors.py +237 -237
- gaia/agents/base/mcp_agent.py +86 -86
- gaia/agents/base/tools.py +88 -83
- gaia/agents/blender/__init__.py +7 -0
- gaia/agents/blender/agent.py +553 -556
- gaia/agents/blender/agent_simple.py +133 -135
- gaia/agents/blender/app.py +211 -211
- gaia/agents/blender/app_simple.py +41 -41
- gaia/agents/blender/core/__init__.py +16 -16
- gaia/agents/blender/core/materials.py +506 -506
- gaia/agents/blender/core/objects.py +316 -316
- gaia/agents/blender/core/rendering.py +225 -225
- gaia/agents/blender/core/scene.py +220 -220
- gaia/agents/blender/core/view.py +146 -146
- gaia/agents/chat/__init__.py +9 -9
- gaia/agents/chat/agent.py +809 -835
- gaia/agents/chat/app.py +1065 -1058
- gaia/agents/chat/session.py +508 -508
- gaia/agents/chat/tools/__init__.py +15 -15
- gaia/agents/chat/tools/file_tools.py +96 -96
- gaia/agents/chat/tools/rag_tools.py +1744 -1729
- gaia/agents/chat/tools/shell_tools.py +437 -436
- gaia/agents/code/__init__.py +7 -7
- gaia/agents/code/agent.py +549 -549
- gaia/agents/code/cli.py +377 -0
- gaia/agents/code/models.py +135 -135
- gaia/agents/code/orchestration/__init__.py +24 -24
- gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
- gaia/agents/code/orchestration/checklist_generator.py +713 -713
- gaia/agents/code/orchestration/factories/__init__.py +9 -9
- gaia/agents/code/orchestration/factories/base.py +63 -63
- gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
- gaia/agents/code/orchestration/factories/python_factory.py +106 -106
- gaia/agents/code/orchestration/orchestrator.py +841 -841
- gaia/agents/code/orchestration/project_analyzer.py +391 -391
- gaia/agents/code/orchestration/steps/__init__.py +67 -67
- gaia/agents/code/orchestration/steps/base.py +188 -188
- gaia/agents/code/orchestration/steps/error_handler.py +314 -314
- gaia/agents/code/orchestration/steps/nextjs.py +828 -828
- gaia/agents/code/orchestration/steps/python.py +307 -307
- gaia/agents/code/orchestration/template_catalog.py +469 -469
- gaia/agents/code/orchestration/workflows/__init__.py +14 -14
- gaia/agents/code/orchestration/workflows/base.py +80 -80
- gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
- gaia/agents/code/orchestration/workflows/python.py +94 -94
- gaia/agents/code/prompts/__init__.py +11 -11
- gaia/agents/code/prompts/base_prompt.py +77 -77
- gaia/agents/code/prompts/code_patterns.py +2034 -2036
- gaia/agents/code/prompts/nextjs_prompt.py +40 -40
- gaia/agents/code/prompts/python_prompt.py +109 -109
- gaia/agents/code/schema_inference.py +365 -365
- gaia/agents/code/system_prompt.py +41 -41
- gaia/agents/code/tools/__init__.py +42 -42
- gaia/agents/code/tools/cli_tools.py +1138 -1138
- gaia/agents/code/tools/code_formatting.py +319 -319
- gaia/agents/code/tools/code_tools.py +769 -769
- gaia/agents/code/tools/error_fixing.py +1347 -1347
- gaia/agents/code/tools/external_tools.py +180 -180
- gaia/agents/code/tools/file_io.py +845 -845
- gaia/agents/code/tools/prisma_tools.py +190 -190
- gaia/agents/code/tools/project_management.py +1016 -1016
- gaia/agents/code/tools/testing.py +321 -321
- gaia/agents/code/tools/typescript_tools.py +122 -122
- gaia/agents/code/tools/validation_parsing.py +461 -461
- gaia/agents/code/tools/validation_tools.py +806 -806
- gaia/agents/code/tools/web_dev_tools.py +1758 -1758
- gaia/agents/code/validators/__init__.py +16 -16
- gaia/agents/code/validators/antipattern_checker.py +241 -241
- gaia/agents/code/validators/ast_analyzer.py +197 -197
- gaia/agents/code/validators/requirements_validator.py +145 -145
- gaia/agents/code/validators/syntax_validator.py +171 -171
- gaia/agents/docker/__init__.py +7 -7
- gaia/agents/docker/agent.py +643 -642
- gaia/agents/emr/__init__.py +8 -8
- gaia/agents/emr/agent.py +1504 -1506
- gaia/agents/emr/cli.py +1322 -1322
- gaia/agents/emr/constants.py +475 -475
- gaia/agents/emr/dashboard/__init__.py +4 -4
- gaia/agents/emr/dashboard/server.py +1972 -1974
- gaia/agents/jira/__init__.py +11 -11
- gaia/agents/jira/agent.py +894 -894
- gaia/agents/jira/jql_templates.py +299 -299
- gaia/agents/routing/__init__.py +7 -7
- gaia/agents/routing/agent.py +567 -570
- gaia/agents/routing/system_prompt.py +75 -75
- gaia/agents/summarize/__init__.py +11 -0
- gaia/agents/summarize/agent.py +885 -0
- gaia/agents/summarize/prompts.py +129 -0
- gaia/api/__init__.py +23 -23
- gaia/api/agent_registry.py +238 -238
- gaia/api/app.py +305 -305
- gaia/api/openai_server.py +575 -575
- gaia/api/schemas.py +186 -186
- gaia/api/sse_handler.py +373 -373
- gaia/apps/__init__.py +4 -4
- gaia/apps/llm/__init__.py +6 -6
- gaia/apps/llm/app.py +184 -169
- gaia/apps/summarize/app.py +116 -633
- gaia/apps/summarize/html_viewer.py +133 -133
- gaia/apps/summarize/pdf_formatter.py +284 -284
- gaia/audio/__init__.py +2 -2
- gaia/audio/audio_client.py +439 -439
- gaia/audio/audio_recorder.py +269 -269
- gaia/audio/kokoro_tts.py +599 -599
- gaia/audio/whisper_asr.py +432 -432
- gaia/chat/__init__.py +16 -16
- gaia/chat/app.py +428 -430
- gaia/chat/prompts.py +522 -522
- gaia/chat/sdk.py +1228 -1225
- gaia/cli.py +5659 -5632
- gaia/database/__init__.py +10 -10
- gaia/database/agent.py +176 -176
- gaia/database/mixin.py +290 -290
- gaia/database/testing.py +64 -64
- gaia/eval/batch_experiment.py +2332 -2332
- gaia/eval/claude.py +542 -542
- gaia/eval/config.py +37 -37
- gaia/eval/email_generator.py +512 -512
- gaia/eval/eval.py +3179 -3179
- gaia/eval/groundtruth.py +1130 -1130
- gaia/eval/transcript_generator.py +582 -582
- gaia/eval/webapp/README.md +167 -167
- gaia/eval/webapp/package-lock.json +875 -875
- gaia/eval/webapp/package.json +20 -20
- gaia/eval/webapp/public/app.js +3402 -3402
- gaia/eval/webapp/public/index.html +87 -87
- gaia/eval/webapp/public/styles.css +3661 -3661
- gaia/eval/webapp/server.js +415 -415
- gaia/eval/webapp/test-setup.js +72 -72
- gaia/installer/__init__.py +23 -0
- gaia/installer/init_command.py +1275 -0
- gaia/installer/lemonade_installer.py +619 -0
- gaia/llm/__init__.py +10 -2
- gaia/llm/base_client.py +60 -0
- gaia/llm/exceptions.py +12 -0
- gaia/llm/factory.py +70 -0
- gaia/llm/lemonade_client.py +3421 -3221
- gaia/llm/lemonade_manager.py +294 -294
- gaia/llm/providers/__init__.py +9 -0
- gaia/llm/providers/claude.py +108 -0
- gaia/llm/providers/lemonade.py +118 -0
- gaia/llm/providers/openai_provider.py +79 -0
- gaia/llm/vlm_client.py +382 -382
- gaia/logger.py +189 -189
- gaia/mcp/agent_mcp_server.py +245 -245
- gaia/mcp/blender_mcp_client.py +138 -138
- gaia/mcp/blender_mcp_server.py +648 -648
- gaia/mcp/context7_cache.py +332 -332
- gaia/mcp/external_services.py +518 -518
- gaia/mcp/mcp_bridge.py +811 -550
- gaia/mcp/servers/__init__.py +6 -6
- gaia/mcp/servers/docker_mcp.py +83 -83
- gaia/perf_analysis.py +361 -0
- gaia/rag/__init__.py +10 -10
- gaia/rag/app.py +293 -293
- gaia/rag/demo.py +304 -304
- gaia/rag/pdf_utils.py +235 -235
- gaia/rag/sdk.py +2194 -2194
- gaia/security.py +183 -163
- gaia/talk/app.py +287 -289
- gaia/talk/sdk.py +538 -538
- gaia/testing/__init__.py +87 -87
- gaia/testing/assertions.py +330 -330
- gaia/testing/fixtures.py +333 -333
- gaia/testing/mocks.py +493 -493
- gaia/util.py +46 -46
- gaia/utils/__init__.py +33 -33
- gaia/utils/file_watcher.py +675 -675
- gaia/utils/parsing.py +223 -223
- gaia/version.py +100 -100
- amd_gaia-0.15.0.dist-info/RECORD +0 -168
- gaia/agents/code/app.py +0 -266
- gaia/llm/llm_client.py +0 -723
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/top_level.txt +0 -0
|
@@ -1,506 +1,506 @@
|
|
|
1
|
-
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
from typing import Dict
|
|
5
|
-
|
|
6
|
-
from gaia.mcp.blender_mcp_client import MCPClient
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class MaterialManager:
|
|
10
|
-
"""Manages Blender material operations."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, mcp: MCPClient):
|
|
13
|
-
self.mcp = mcp
|
|
14
|
-
|
|
15
|
-
def create_ground_material(
|
|
16
|
-
self, ground_texture_name: str, maps_texture_name: str
|
|
17
|
-
) -> Dict:
|
|
18
|
-
"""Create the ground material with separate land and water shaders, using displacement."""
|
|
19
|
-
|
|
20
|
-
def generate_code():
|
|
21
|
-
return f"""
|
|
22
|
-
import bpy
|
|
23
|
-
|
|
24
|
-
# Get the Earth object
|
|
25
|
-
earth = bpy.data.objects.get("Earth")
|
|
26
|
-
if not earth:
|
|
27
|
-
print("Error: Earth object not found")
|
|
28
|
-
exit()
|
|
29
|
-
|
|
30
|
-
# Create new material
|
|
31
|
-
ground_mat = bpy.data.materials.new(name="ground")
|
|
32
|
-
ground_mat.use_nodes = True
|
|
33
|
-
earth.data.materials.append(ground_mat)
|
|
34
|
-
|
|
35
|
-
# Get material nodes and links
|
|
36
|
-
nodes = ground_mat.node_tree.nodes
|
|
37
|
-
links = ground_mat.node_tree.links
|
|
38
|
-
|
|
39
|
-
# Clear default nodes
|
|
40
|
-
for node in nodes:
|
|
41
|
-
nodes.remove(node)
|
|
42
|
-
|
|
43
|
-
# Create nodes for ground material
|
|
44
|
-
output = nodes.new(type='ShaderNodeOutputMaterial')
|
|
45
|
-
output.location = (800, 0)
|
|
46
|
-
|
|
47
|
-
# Add texture image for Earth ground
|
|
48
|
-
tex_ground = nodes.new(type='ShaderNodeTexImage')
|
|
49
|
-
tex_ground.location = (-600, 200)
|
|
50
|
-
tex_ground.image = bpy.data.images.get("{ground_texture_name}")
|
|
51
|
-
tex_ground.projection = 'SPHERE'
|
|
52
|
-
tex_ground.interpolation = 'Linear' # Updated to Linear as shown in screenshot
|
|
53
|
-
|
|
54
|
-
# Add texture coordinate
|
|
55
|
-
tex_coord = nodes.new(type='ShaderNodeTexCoord')
|
|
56
|
-
tex_coord.location = (-900, 0)
|
|
57
|
-
|
|
58
|
-
# Earth maps (water mask and displacement)
|
|
59
|
-
tex_maps = nodes.new(type='ShaderNodeTexImage')
|
|
60
|
-
tex_maps.location = (-600, -200)
|
|
61
|
-
tex_maps.image = bpy.data.images.get("{maps_texture_name}")
|
|
62
|
-
tex_maps.projection = 'SPHERE'
|
|
63
|
-
tex_maps.interpolation = 'Linear' # Already set to Linear
|
|
64
|
-
|
|
65
|
-
# Separate RGB for maps
|
|
66
|
-
separate_rgb = nodes.new(type='ShaderNodeSeparateRGB')
|
|
67
|
-
separate_rgb.location = (-300, -200)
|
|
68
|
-
|
|
69
|
-
# Single Principled BSDF with values matching screenshot
|
|
70
|
-
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
71
|
-
principled.location = (400, 200)
|
|
72
|
-
principled.inputs['Metallic'].default_value = 0.030 # As shown in screenshot
|
|
73
|
-
principled.inputs['Roughness'].default_value = 0.500 # As shown in screenshot
|
|
74
|
-
principled.inputs['IOR'].default_value = 1.500 # As shown in screenshot
|
|
75
|
-
principled.inputs['Alpha'].default_value = 1.000 # As shown in screenshot
|
|
76
|
-
|
|
77
|
-
# Alternative approach - keep both land and water shaders as before
|
|
78
|
-
# Land material
|
|
79
|
-
land_shader = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
80
|
-
land_shader.location = (100, 200)
|
|
81
|
-
land_shader.inputs['Specular'].default_value = 0.0
|
|
82
|
-
land_shader.inputs['Roughness'].default_value = 1.0
|
|
83
|
-
|
|
84
|
-
# Water material
|
|
85
|
-
water_shader = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
86
|
-
water_shader.location = (100, -100)
|
|
87
|
-
water_shader.inputs['Roughness'].default_value = 0.4
|
|
88
|
-
water_shader.inputs['IOR'].default_value = 1.333
|
|
89
|
-
|
|
90
|
-
# Mix shader
|
|
91
|
-
mix_shader = nodes.new(type='ShaderNodeMixShader')
|
|
92
|
-
mix_shader.location = (500, 0)
|
|
93
|
-
|
|
94
|
-
# Displacement node
|
|
95
|
-
displace = nodes.new(type='ShaderNodeDisplacement')
|
|
96
|
-
displace.location = (500, -300)
|
|
97
|
-
displace.inputs['Scale'].default_value = 0.005 # Match tutorial value for displacement
|
|
98
|
-
|
|
99
|
-
# Connect nodes
|
|
100
|
-
# Option 1: Using single Principled BSDF (as shown in screenshot)
|
|
101
|
-
links.new(tex_coord.outputs['Generated'], tex_ground.inputs['Vector'])
|
|
102
|
-
links.new(tex_coord.outputs['Generated'], tex_maps.inputs['Vector'])
|
|
103
|
-
links.new(tex_ground.outputs['Color'], principled.inputs['Base Color'])
|
|
104
|
-
links.new(tex_maps.outputs['Color'], separate_rgb.inputs['Image'])
|
|
105
|
-
links.new(separate_rgb.outputs['R'], displace.inputs['Height']) # R (red) channel is height
|
|
106
|
-
links.new(principled.outputs['BSDF'], output.inputs['Surface'])
|
|
107
|
-
links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
|
|
108
|
-
|
|
109
|
-
# Set material displacement method to match tutorial
|
|
110
|
-
ground_mat.displacement_method = 'DISPLACEMENT'
|
|
111
|
-
|
|
112
|
-
print("Ground material created exactly as shown in screenshot")
|
|
113
|
-
"""
|
|
114
|
-
|
|
115
|
-
return self.mcp.execute_code(generate_code())
|
|
116
|
-
|
|
117
|
-
def create_atmosphere_material(self) -> Dict:
|
|
118
|
-
"""Create the atmosphere material with volume scatter."""
|
|
119
|
-
|
|
120
|
-
def generate_code():
|
|
121
|
-
return """
|
|
122
|
-
import bpy
|
|
123
|
-
|
|
124
|
-
# Create atmosphere material
|
|
125
|
-
atm_mat = bpy.data.materials.new(name="atmosphere")
|
|
126
|
-
atm_mat.use_nodes = True
|
|
127
|
-
|
|
128
|
-
# Get material nodes
|
|
129
|
-
nodes = atm_mat.node_tree.nodes
|
|
130
|
-
links = atm_mat.node_tree.links
|
|
131
|
-
|
|
132
|
-
# Clear default nodes
|
|
133
|
-
for node in nodes:
|
|
134
|
-
nodes.remove(node)
|
|
135
|
-
|
|
136
|
-
# Create nodes for atmosphere material as shown in tutorial
|
|
137
|
-
output = nodes.new(type='ShaderNodeOutputMaterial')
|
|
138
|
-
output.location = (800, 0)
|
|
139
|
-
|
|
140
|
-
# Add texture coordinate for atmosphere
|
|
141
|
-
tex_coord = nodes.new(type='ShaderNodeTexCoord')
|
|
142
|
-
tex_coord.location = (-900, 0)
|
|
143
|
-
|
|
144
|
-
# Add volume scatter - match tutorial color exactly
|
|
145
|
-
volume_scatter = nodes.new(type='ShaderNodeVolumeScatter')
|
|
146
|
-
volume_scatter.location = (500, 200)
|
|
147
|
-
volume_scatter.inputs['Color'].default_value = (0.3, 0.6, 1.0, 1.0) # Blue color from tutorial
|
|
148
|
-
|
|
149
|
-
# Value for atmosphere thickness - 1% of planet radius as in tutorial
|
|
150
|
-
thickness = nodes.new(type='ShaderNodeValue')
|
|
151
|
-
thickness.location = (-600, -300)
|
|
152
|
-
thickness.outputs[0].default_value = 0.01 # 1% of planet radius as specified in tutorial
|
|
153
|
-
|
|
154
|
-
# Vector length for atmosphere density gradient
|
|
155
|
-
vec_math = nodes.new(type='ShaderNodeVectorMath')
|
|
156
|
-
vec_math.location = (-600, 0)
|
|
157
|
-
vec_math.operation = 'LENGTH'
|
|
158
|
-
|
|
159
|
-
# Subtract 1 to get 0 at surface level
|
|
160
|
-
math_sub = nodes.new(type='ShaderNodeMath')
|
|
161
|
-
math_sub.location = (-400, 0)
|
|
162
|
-
math_sub.operation = 'SUBTRACT'
|
|
163
|
-
math_sub.inputs[1].default_value = 1.0
|
|
164
|
-
|
|
165
|
-
# Divide by thickness to normalize distance - exactly as in tutorial
|
|
166
|
-
math_div = nodes.new(type='ShaderNodeMath')
|
|
167
|
-
math_div.location = (-200, 0)
|
|
168
|
-
math_div.operation = 'DIVIDE'
|
|
169
|
-
math_div.use_clamp = True
|
|
170
|
-
|
|
171
|
-
# Multiply by 15 for density adjustment - exact value from tutorial
|
|
172
|
-
math_mul1 = nodes.new(type='ShaderNodeMath')
|
|
173
|
-
math_mul1.location = (0, 0)
|
|
174
|
-
math_mul1.operation = 'MULTIPLY'
|
|
175
|
-
math_mul1.inputs[1].default_value = 15.0 # Tutorial uses exactly 15
|
|
176
|
-
|
|
177
|
-
# Power for exponential falloff - uses e (Euler's number) in tutorial
|
|
178
|
-
math_pow = nodes.new(type='ShaderNodeMath')
|
|
179
|
-
math_pow.location = (200, 0)
|
|
180
|
-
math_pow.operation = 'POWER'
|
|
181
|
-
math_pow.inputs[1].default_value = 1.0 # Power of e (Euler's number)
|
|
182
|
-
|
|
183
|
-
# Multiply by 0.05 for final density - exact value from tutorial
|
|
184
|
-
math_mul2 = nodes.new(type='ShaderNodeMath')
|
|
185
|
-
math_mul2.location = (400, 0)
|
|
186
|
-
math_mul2.operation = 'MULTIPLY'
|
|
187
|
-
math_mul2.inputs[1].default_value = 0.05 # Tutorial uses exactly 0.05
|
|
188
|
-
|
|
189
|
-
# Displacement for atmosphere
|
|
190
|
-
displace = nodes.new(type='ShaderNodeDisplacement')
|
|
191
|
-
displace.location = (500, -200)
|
|
192
|
-
displace.inputs['Scale'].default_value = 1.0
|
|
193
|
-
|
|
194
|
-
# Connect nodes exactly as demonstrated in tutorial
|
|
195
|
-
links.new(tex_coord.outputs['Object'], vec_math.inputs[0])
|
|
196
|
-
links.new(vec_math.outputs[0], math_sub.inputs[0])
|
|
197
|
-
links.new(math_sub.outputs[0], math_div.inputs[0])
|
|
198
|
-
links.new(thickness.outputs[0], math_div.inputs[1])
|
|
199
|
-
links.new(math_div.outputs[0], math_mul1.inputs[0])
|
|
200
|
-
links.new(math_mul1.outputs[0], math_pow.inputs[0])
|
|
201
|
-
links.new(math_pow.outputs[0], math_mul2.inputs[0])
|
|
202
|
-
links.new(math_mul2.outputs[0], volume_scatter.inputs['Density'])
|
|
203
|
-
links.new(thickness.outputs[0], displace.inputs['Scale'])
|
|
204
|
-
links.new(volume_scatter.outputs['Volume'], output.inputs['Volume'])
|
|
205
|
-
links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
|
|
206
|
-
|
|
207
|
-
# Set material displacement method as in tutorial
|
|
208
|
-
atm_mat.displacement_method = 'DISPLACEMENT'
|
|
209
|
-
|
|
210
|
-
print("Atmosphere material created exactly as in tutorial")
|
|
211
|
-
"""
|
|
212
|
-
|
|
213
|
-
return self.mcp.execute_code(generate_code())
|
|
214
|
-
|
|
215
|
-
def create_clouds_material(self, clouds_texture_name: str) -> Dict:
|
|
216
|
-
"""Create the clouds material with subsurface scattering."""
|
|
217
|
-
|
|
218
|
-
def generate_code():
|
|
219
|
-
return f"""
|
|
220
|
-
import bpy
|
|
221
|
-
|
|
222
|
-
# Create clouds material
|
|
223
|
-
clouds_mat = bpy.data.materials.new(name="clouds")
|
|
224
|
-
clouds_mat.use_nodes = True
|
|
225
|
-
|
|
226
|
-
# Get material nodes
|
|
227
|
-
nodes = clouds_mat.node_tree.nodes
|
|
228
|
-
links = clouds_mat.node_tree.links
|
|
229
|
-
|
|
230
|
-
# Clear default nodes
|
|
231
|
-
for node in nodes:
|
|
232
|
-
nodes.remove(node)
|
|
233
|
-
|
|
234
|
-
# Create nodes for clouds material - match tutorial exactly
|
|
235
|
-
output = nodes.new(type='ShaderNodeOutputMaterial')
|
|
236
|
-
output.location = (1000, 0)
|
|
237
|
-
|
|
238
|
-
# Add texture coordinate for clouds
|
|
239
|
-
tex_coord = nodes.new(type='ShaderNodeTexCoord')
|
|
240
|
-
tex_coord.location = (-900, 0)
|
|
241
|
-
|
|
242
|
-
# Add cloud texture
|
|
243
|
-
tex_clouds = nodes.new(type='ShaderNodeTexImage')
|
|
244
|
-
tex_clouds.location = (-600, 0)
|
|
245
|
-
tex_clouds.image = bpy.data.images.get("{clouds_texture_name}")
|
|
246
|
-
tex_clouds.projection = 'SPHERE'
|
|
247
|
-
tex_clouds.interpolation = 'Linear' # Match tutorial setting
|
|
248
|
-
|
|
249
|
-
# Gamma correction for cloud texture - exactly 0.5 as in tutorial
|
|
250
|
-
gamma = nodes.new(type='ShaderNodeGamma')
|
|
251
|
-
gamma.location = (-400, 0)
|
|
252
|
-
gamma.inputs['Gamma'].default_value = 0.5 # Exactly as in tutorial
|
|
253
|
-
|
|
254
|
-
# Second gamma correction - exactly 0.9 as in tutorial
|
|
255
|
-
gamma2 = nodes.new(type='ShaderNodeGamma')
|
|
256
|
-
gamma2.location = (-200, 0)
|
|
257
|
-
gamma2.inputs['Gamma'].default_value = 0.9 # Exactly as in tutorial
|
|
258
|
-
|
|
259
|
-
# Add Transparent BSDF
|
|
260
|
-
transparent = nodes.new(type='ShaderNodeBsdfTransparent')
|
|
261
|
-
transparent.location = (400, 100)
|
|
262
|
-
|
|
263
|
-
# Add Subsurface Scattering BSDF - exactly as in tutorial
|
|
264
|
-
subsurface = nodes.new(type='ShaderNodeSubsurfaceScattering')
|
|
265
|
-
subsurface.location = (400, -100)
|
|
266
|
-
subsurface.inputs['Radius'].default_value = (1, 1, 1) # Tutorial setting
|
|
267
|
-
|
|
268
|
-
# Multiply for cloud intensity - exactly 5.0 as in tutorial
|
|
269
|
-
math_mul = nodes.new(type='ShaderNodeMath')
|
|
270
|
-
math_mul.location = (0, -200)
|
|
271
|
-
math_mul.operation = 'MULTIPLY'
|
|
272
|
-
math_mul.inputs[1].default_value = 5.0 # Exactly as in tutorial
|
|
273
|
-
|
|
274
|
-
# Power for cloud exponential control - as in tutorial
|
|
275
|
-
math_pow = nodes.new(type='ShaderNodeMath')
|
|
276
|
-
math_pow.location = (200, -200)
|
|
277
|
-
math_pow.operation = 'POWER'
|
|
278
|
-
math_pow.inputs[1].default_value = 1.0 # Tutorial setting
|
|
279
|
-
|
|
280
|
-
# Mix shader
|
|
281
|
-
mix_shader = nodes.new(type='ShaderNodeMixShader')
|
|
282
|
-
mix_shader.location = (700, 0)
|
|
283
|
-
|
|
284
|
-
# Displacement node - exactly 0.005 as in tutorial
|
|
285
|
-
displace = nodes.new(type='ShaderNodeDisplacement')
|
|
286
|
-
displace.location = (700, -300)
|
|
287
|
-
displace.inputs['Scale'].default_value = 0.005 # Exactly as in tutorial
|
|
288
|
-
|
|
289
|
-
# Connect nodes exactly as in tutorial
|
|
290
|
-
links.new(tex_coord.outputs['Generated'], tex_clouds.inputs['Vector'])
|
|
291
|
-
links.new(tex_clouds.outputs['Color'], gamma.inputs['Color'])
|
|
292
|
-
links.new(gamma.outputs['Color'], gamma2.inputs['Color'])
|
|
293
|
-
links.new(gamma2.outputs['Color'], subsurface.inputs['Color'])
|
|
294
|
-
links.new(gamma2.outputs['Color'], math_mul.inputs[0])
|
|
295
|
-
links.new(math_mul.outputs[0], math_pow.inputs[0])
|
|
296
|
-
links.new(math_pow.outputs[0], mix_shader.inputs['Fac'])
|
|
297
|
-
links.new(transparent.outputs['BSDF'], mix_shader.inputs[1])
|
|
298
|
-
links.new(subsurface.outputs['BSSRDF'], mix_shader.inputs[2])
|
|
299
|
-
links.new(tex_clouds.outputs['Color'], displace.inputs['Height'])
|
|
300
|
-
links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])
|
|
301
|
-
links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
|
|
302
|
-
|
|
303
|
-
# Set material displacement method as in tutorial
|
|
304
|
-
clouds_mat.displacement_method = 'BUMP' # Tutorial setting
|
|
305
|
-
|
|
306
|
-
print("Clouds material created exactly as in tutorial")
|
|
307
|
-
"""
|
|
308
|
-
|
|
309
|
-
return self.mcp.execute_code(generate_code())
|
|
310
|
-
|
|
311
|
-
def set_material_color(self, object_name: str, color: tuple = (1, 0, 0, 1)) -> Dict:
|
|
312
|
-
"""
|
|
313
|
-
Set the material color for an object. Creates a new material if one doesn't exist.
|
|
314
|
-
|
|
315
|
-
Args:
|
|
316
|
-
object_name: Name of the object to modify
|
|
317
|
-
color: RGBA color values as tuple (red, green, blue, alpha), values from 0-1
|
|
318
|
-
|
|
319
|
-
Returns:
|
|
320
|
-
Dictionary with the operation result
|
|
321
|
-
"""
|
|
322
|
-
|
|
323
|
-
def generate_code():
|
|
324
|
-
return f"""
|
|
325
|
-
import bpy
|
|
326
|
-
|
|
327
|
-
result = {{"status": "processing"}}
|
|
328
|
-
|
|
329
|
-
# Get the object
|
|
330
|
-
obj = bpy.data.objects.get("{object_name}")
|
|
331
|
-
if not obj:
|
|
332
|
-
result = {{"status": "error", "error": f"Object '{object_name}' not found"}}
|
|
333
|
-
else:
|
|
334
|
-
# Create a new material if needed
|
|
335
|
-
mat_name = "{object_name}_material"
|
|
336
|
-
if mat_name in bpy.data.materials:
|
|
337
|
-
mat = bpy.data.materials[mat_name]
|
|
338
|
-
else:
|
|
339
|
-
mat = bpy.data.materials.new(name=mat_name)
|
|
340
|
-
|
|
341
|
-
# Enable nodes for the material
|
|
342
|
-
mat.use_nodes = True
|
|
343
|
-
nodes = mat.node_tree.nodes
|
|
344
|
-
|
|
345
|
-
# Clear existing nodes
|
|
346
|
-
for node in nodes:
|
|
347
|
-
nodes.remove(node)
|
|
348
|
-
|
|
349
|
-
# Create new nodes
|
|
350
|
-
output = nodes.new(type='ShaderNodeOutputMaterial')
|
|
351
|
-
output.location = (300, 0)
|
|
352
|
-
|
|
353
|
-
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
354
|
-
bsdf.location = (0, 0)
|
|
355
|
-
|
|
356
|
-
# Set color - use correct format for Blender 4.0
|
|
357
|
-
bsdf.inputs["Base Color"].default_value = {color}
|
|
358
|
-
|
|
359
|
-
# Also set viewport display color for material
|
|
360
|
-
mat.diffuse_color = {color}
|
|
361
|
-
|
|
362
|
-
# Connect nodes
|
|
363
|
-
mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
|
|
364
|
-
|
|
365
|
-
# Assign material to object
|
|
366
|
-
if len(obj.data.materials) == 0:
|
|
367
|
-
obj.data.materials.append(mat)
|
|
368
|
-
else:
|
|
369
|
-
obj.data.materials[0] = mat
|
|
370
|
-
|
|
371
|
-
# Set the active material slot
|
|
372
|
-
obj.active_material_index = 0
|
|
373
|
-
obj.active_material = mat
|
|
374
|
-
|
|
375
|
-
# Set the render engine to show materials properly
|
|
376
|
-
# For Blender 4.0, use valid enum values
|
|
377
|
-
if hasattr(bpy.context.scene, 'render'):
|
|
378
|
-
if hasattr(bpy.context.scene.render, 'engine'):
|
|
379
|
-
current_engine = bpy.context.scene.render.engine
|
|
380
|
-
if current_engine not in ['CYCLES', 'BLENDER_EEVEE_NEXT', 'BLENDER_WORKBENCH']:
|
|
381
|
-
try:
|
|
382
|
-
bpy.context.scene.render.engine = 'CYCLES'
|
|
383
|
-
except Exception as e:
|
|
384
|
-
print(f"Could not set render engine: {{e}}")
|
|
385
|
-
|
|
386
|
-
# Force update of all 3D viewports
|
|
387
|
-
for window in bpy.context.window_manager.windows:
|
|
388
|
-
for area in window.screen.areas:
|
|
389
|
-
if area.type == 'VIEW_3D':
|
|
390
|
-
area.tag_redraw()
|
|
391
|
-
|
|
392
|
-
result = {{
|
|
393
|
-
"status": "success",
|
|
394
|
-
"message": "Material color set successfully",
|
|
395
|
-
"debug_info": {{
|
|
396
|
-
"object_name": obj.name,
|
|
397
|
-
"material_name": mat.name,
|
|
398
|
-
"color_set": list({color}),
|
|
399
|
-
"material_slots": len(obj.material_slots),
|
|
400
|
-
"active_material": obj.active_material.name if obj.active_material else None,
|
|
401
|
-
"render_engine": bpy.context.scene.render.engine if hasattr(bpy.context.scene, 'render') else "unknown"
|
|
402
|
-
}}
|
|
403
|
-
}}
|
|
404
|
-
|
|
405
|
-
result
|
|
406
|
-
"""
|
|
407
|
-
|
|
408
|
-
return self.mcp.execute_code(generate_code())
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def generate_material_assignment_code(
|
|
412
|
-
object_name: str, material_name: str = None, color: tuple = (0.8, 0.8, 0.8, 1.0)
|
|
413
|
-
) -> str:
|
|
414
|
-
"""
|
|
415
|
-
Generates Python code to create a material and assign it to an object with proper error handling.
|
|
416
|
-
|
|
417
|
-
Args:
|
|
418
|
-
object_name: Name of the object to assign material to
|
|
419
|
-
material_name: Name for the new material (default: derived from object name)
|
|
420
|
-
color: RGBA color tuple (r, g, b, a) with values from 0.0 to 1.0
|
|
421
|
-
|
|
422
|
-
Returns:
|
|
423
|
-
String containing Python code that can be executed in Blender
|
|
424
|
-
"""
|
|
425
|
-
if material_name is None:
|
|
426
|
-
material_name = f"{object_name}_material"
|
|
427
|
-
|
|
428
|
-
# Build a code block with proper error handling
|
|
429
|
-
code = f"""
|
|
430
|
-
import bpy
|
|
431
|
-
|
|
432
|
-
result = {{"status": "processing"}}
|
|
433
|
-
|
|
434
|
-
# Get the object
|
|
435
|
-
obj = bpy.data.objects.get('{object_name}')
|
|
436
|
-
if not obj:
|
|
437
|
-
result = {{"status": "error", "message": "Object '{object_name}' not found"}}
|
|
438
|
-
else:
|
|
439
|
-
# Create the material if it doesn't exist
|
|
440
|
-
mat = bpy.data.materials.get('{material_name}')
|
|
441
|
-
if not mat:
|
|
442
|
-
mat = bpy.data.materials.new(name="{material_name}")
|
|
443
|
-
|
|
444
|
-
# Set the color
|
|
445
|
-
mat.diffuse_color = {color}
|
|
446
|
-
|
|
447
|
-
# Assign material to object
|
|
448
|
-
if len(obj.data.materials) == 0:
|
|
449
|
-
obj.data.materials.append(mat)
|
|
450
|
-
else:
|
|
451
|
-
obj.data.materials[0] = mat
|
|
452
|
-
|
|
453
|
-
result = {{
|
|
454
|
-
"status": "success",
|
|
455
|
-
"message": "Material created and assigned",
|
|
456
|
-
"object": "{object_name}",
|
|
457
|
-
"material": "{material_name}"
|
|
458
|
-
}}
|
|
459
|
-
|
|
460
|
-
result
|
|
461
|
-
"""
|
|
462
|
-
return code
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
def generate_materials_for_all_objects_code() -> str:
|
|
466
|
-
"""
|
|
467
|
-
Generates Python code that creates default materials for all objects in the scene
|
|
468
|
-
that don't already have materials assigned.
|
|
469
|
-
|
|
470
|
-
Returns:
|
|
471
|
-
String containing Python code that can be executed in Blender
|
|
472
|
-
"""
|
|
473
|
-
code = """
|
|
474
|
-
import bpy
|
|
475
|
-
|
|
476
|
-
result = {"status": "processing", "created": 0, "objects": []}
|
|
477
|
-
|
|
478
|
-
# Process all objects in the scene
|
|
479
|
-
for obj in bpy.data.objects:
|
|
480
|
-
# Skip objects that can't have materials or already have materials
|
|
481
|
-
if obj.type not in {'MESH', 'CURVE', 'SURFACE', 'META', 'FONT'} or len(obj.material_slots) > 0 and obj.active_material:
|
|
482
|
-
continue
|
|
483
|
-
|
|
484
|
-
# Create a new material for this object
|
|
485
|
-
mat_name = f"{obj.name}_material"
|
|
486
|
-
mat = bpy.data.materials.new(name=mat_name)
|
|
487
|
-
|
|
488
|
-
# Set default color based on object name (for consistent results)
|
|
489
|
-
import hashlib
|
|
490
|
-
name_hash = int(hashlib.md5(obj.name.encode()).hexdigest(), 16)
|
|
491
|
-
r = ((name_hash & 0xFF0000) >> 16) / 255.0
|
|
492
|
-
g = ((name_hash & 0x00FF00) >> 8) / 255.0
|
|
493
|
-
b = (name_hash & 0x0000FF) / 255.0
|
|
494
|
-
mat.diffuse_color = (r, g, b, 1.0)
|
|
495
|
-
|
|
496
|
-
# Assign the material to the object
|
|
497
|
-
obj.data.materials.append(mat)
|
|
498
|
-
|
|
499
|
-
result["objects"].append({"name": obj.name, "material": mat_name})
|
|
500
|
-
result["created"] += 1
|
|
501
|
-
|
|
502
|
-
result["status"] = "success"
|
|
503
|
-
result["message"] = f"Created materials for {result['created']} objects"
|
|
504
|
-
result
|
|
505
|
-
"""
|
|
506
|
-
return code
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from gaia.mcp.blender_mcp_client import MCPClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MaterialManager:
|
|
10
|
+
"""Manages Blender material operations."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, mcp: MCPClient):
|
|
13
|
+
self.mcp = mcp
|
|
14
|
+
|
|
15
|
+
def create_ground_material(
|
|
16
|
+
self, ground_texture_name: str, maps_texture_name: str
|
|
17
|
+
) -> Dict:
|
|
18
|
+
"""Create the ground material with separate land and water shaders, using displacement."""
|
|
19
|
+
|
|
20
|
+
def generate_code():
|
|
21
|
+
return f"""
|
|
22
|
+
import bpy
|
|
23
|
+
|
|
24
|
+
# Get the Earth object
|
|
25
|
+
earth = bpy.data.objects.get("Earth")
|
|
26
|
+
if not earth:
|
|
27
|
+
print("Error: Earth object not found")
|
|
28
|
+
exit()
|
|
29
|
+
|
|
30
|
+
# Create new material
|
|
31
|
+
ground_mat = bpy.data.materials.new(name="ground")
|
|
32
|
+
ground_mat.use_nodes = True
|
|
33
|
+
earth.data.materials.append(ground_mat)
|
|
34
|
+
|
|
35
|
+
# Get material nodes and links
|
|
36
|
+
nodes = ground_mat.node_tree.nodes
|
|
37
|
+
links = ground_mat.node_tree.links
|
|
38
|
+
|
|
39
|
+
# Clear default nodes
|
|
40
|
+
for node in nodes:
|
|
41
|
+
nodes.remove(node)
|
|
42
|
+
|
|
43
|
+
# Create nodes for ground material
|
|
44
|
+
output = nodes.new(type='ShaderNodeOutputMaterial')
|
|
45
|
+
output.location = (800, 0)
|
|
46
|
+
|
|
47
|
+
# Add texture image for Earth ground
|
|
48
|
+
tex_ground = nodes.new(type='ShaderNodeTexImage')
|
|
49
|
+
tex_ground.location = (-600, 200)
|
|
50
|
+
tex_ground.image = bpy.data.images.get("{ground_texture_name}")
|
|
51
|
+
tex_ground.projection = 'SPHERE'
|
|
52
|
+
tex_ground.interpolation = 'Linear' # Updated to Linear as shown in screenshot
|
|
53
|
+
|
|
54
|
+
# Add texture coordinate
|
|
55
|
+
tex_coord = nodes.new(type='ShaderNodeTexCoord')
|
|
56
|
+
tex_coord.location = (-900, 0)
|
|
57
|
+
|
|
58
|
+
# Earth maps (water mask and displacement)
|
|
59
|
+
tex_maps = nodes.new(type='ShaderNodeTexImage')
|
|
60
|
+
tex_maps.location = (-600, -200)
|
|
61
|
+
tex_maps.image = bpy.data.images.get("{maps_texture_name}")
|
|
62
|
+
tex_maps.projection = 'SPHERE'
|
|
63
|
+
tex_maps.interpolation = 'Linear' # Already set to Linear
|
|
64
|
+
|
|
65
|
+
# Separate RGB for maps
|
|
66
|
+
separate_rgb = nodes.new(type='ShaderNodeSeparateRGB')
|
|
67
|
+
separate_rgb.location = (-300, -200)
|
|
68
|
+
|
|
69
|
+
# Single Principled BSDF with values matching screenshot
|
|
70
|
+
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
71
|
+
principled.location = (400, 200)
|
|
72
|
+
principled.inputs['Metallic'].default_value = 0.030 # As shown in screenshot
|
|
73
|
+
principled.inputs['Roughness'].default_value = 0.500 # As shown in screenshot
|
|
74
|
+
principled.inputs['IOR'].default_value = 1.500 # As shown in screenshot
|
|
75
|
+
principled.inputs['Alpha'].default_value = 1.000 # As shown in screenshot
|
|
76
|
+
|
|
77
|
+
# Alternative approach - keep both land and water shaders as before
|
|
78
|
+
# Land material
|
|
79
|
+
land_shader = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
80
|
+
land_shader.location = (100, 200)
|
|
81
|
+
land_shader.inputs['Specular'].default_value = 0.0
|
|
82
|
+
land_shader.inputs['Roughness'].default_value = 1.0
|
|
83
|
+
|
|
84
|
+
# Water material
|
|
85
|
+
water_shader = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
86
|
+
water_shader.location = (100, -100)
|
|
87
|
+
water_shader.inputs['Roughness'].default_value = 0.4
|
|
88
|
+
water_shader.inputs['IOR'].default_value = 1.333
|
|
89
|
+
|
|
90
|
+
# Mix shader
|
|
91
|
+
mix_shader = nodes.new(type='ShaderNodeMixShader')
|
|
92
|
+
mix_shader.location = (500, 0)
|
|
93
|
+
|
|
94
|
+
# Displacement node
|
|
95
|
+
displace = nodes.new(type='ShaderNodeDisplacement')
|
|
96
|
+
displace.location = (500, -300)
|
|
97
|
+
displace.inputs['Scale'].default_value = 0.005 # Match tutorial value for displacement
|
|
98
|
+
|
|
99
|
+
# Connect nodes
|
|
100
|
+
# Option 1: Using single Principled BSDF (as shown in screenshot)
|
|
101
|
+
links.new(tex_coord.outputs['Generated'], tex_ground.inputs['Vector'])
|
|
102
|
+
links.new(tex_coord.outputs['Generated'], tex_maps.inputs['Vector'])
|
|
103
|
+
links.new(tex_ground.outputs['Color'], principled.inputs['Base Color'])
|
|
104
|
+
links.new(tex_maps.outputs['Color'], separate_rgb.inputs['Image'])
|
|
105
|
+
links.new(separate_rgb.outputs['R'], displace.inputs['Height']) # R (red) channel is height
|
|
106
|
+
links.new(principled.outputs['BSDF'], output.inputs['Surface'])
|
|
107
|
+
links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
|
|
108
|
+
|
|
109
|
+
# Set material displacement method to match tutorial
|
|
110
|
+
ground_mat.displacement_method = 'DISPLACEMENT'
|
|
111
|
+
|
|
112
|
+
print("Ground material created exactly as shown in screenshot")
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
return self.mcp.execute_code(generate_code())
|
|
116
|
+
|
|
117
|
+
def create_atmosphere_material(self) -> Dict:
|
|
118
|
+
"""Create the atmosphere material with volume scatter."""
|
|
119
|
+
|
|
120
|
+
def generate_code():
|
|
121
|
+
return """
|
|
122
|
+
import bpy
|
|
123
|
+
|
|
124
|
+
# Create atmosphere material
|
|
125
|
+
atm_mat = bpy.data.materials.new(name="atmosphere")
|
|
126
|
+
atm_mat.use_nodes = True
|
|
127
|
+
|
|
128
|
+
# Get material nodes
|
|
129
|
+
nodes = atm_mat.node_tree.nodes
|
|
130
|
+
links = atm_mat.node_tree.links
|
|
131
|
+
|
|
132
|
+
# Clear default nodes
|
|
133
|
+
for node in nodes:
|
|
134
|
+
nodes.remove(node)
|
|
135
|
+
|
|
136
|
+
# Create nodes for atmosphere material as shown in tutorial
|
|
137
|
+
output = nodes.new(type='ShaderNodeOutputMaterial')
|
|
138
|
+
output.location = (800, 0)
|
|
139
|
+
|
|
140
|
+
# Add texture coordinate for atmosphere
|
|
141
|
+
tex_coord = nodes.new(type='ShaderNodeTexCoord')
|
|
142
|
+
tex_coord.location = (-900, 0)
|
|
143
|
+
|
|
144
|
+
# Add volume scatter - match tutorial color exactly
|
|
145
|
+
volume_scatter = nodes.new(type='ShaderNodeVolumeScatter')
|
|
146
|
+
volume_scatter.location = (500, 200)
|
|
147
|
+
volume_scatter.inputs['Color'].default_value = (0.3, 0.6, 1.0, 1.0) # Blue color from tutorial
|
|
148
|
+
|
|
149
|
+
# Value for atmosphere thickness - 1% of planet radius as in tutorial
|
|
150
|
+
thickness = nodes.new(type='ShaderNodeValue')
|
|
151
|
+
thickness.location = (-600, -300)
|
|
152
|
+
thickness.outputs[0].default_value = 0.01 # 1% of planet radius as specified in tutorial
|
|
153
|
+
|
|
154
|
+
# Vector length for atmosphere density gradient
|
|
155
|
+
vec_math = nodes.new(type='ShaderNodeVectorMath')
|
|
156
|
+
vec_math.location = (-600, 0)
|
|
157
|
+
vec_math.operation = 'LENGTH'
|
|
158
|
+
|
|
159
|
+
# Subtract 1 to get 0 at surface level
|
|
160
|
+
math_sub = nodes.new(type='ShaderNodeMath')
|
|
161
|
+
math_sub.location = (-400, 0)
|
|
162
|
+
math_sub.operation = 'SUBTRACT'
|
|
163
|
+
math_sub.inputs[1].default_value = 1.0
|
|
164
|
+
|
|
165
|
+
# Divide by thickness to normalize distance - exactly as in tutorial
|
|
166
|
+
math_div = nodes.new(type='ShaderNodeMath')
|
|
167
|
+
math_div.location = (-200, 0)
|
|
168
|
+
math_div.operation = 'DIVIDE'
|
|
169
|
+
math_div.use_clamp = True
|
|
170
|
+
|
|
171
|
+
# Multiply by 15 for density adjustment - exact value from tutorial
|
|
172
|
+
math_mul1 = nodes.new(type='ShaderNodeMath')
|
|
173
|
+
math_mul1.location = (0, 0)
|
|
174
|
+
math_mul1.operation = 'MULTIPLY'
|
|
175
|
+
math_mul1.inputs[1].default_value = 15.0 # Tutorial uses exactly 15
|
|
176
|
+
|
|
177
|
+
# Power for exponential falloff - uses e (Euler's number) in tutorial
|
|
178
|
+
math_pow = nodes.new(type='ShaderNodeMath')
|
|
179
|
+
math_pow.location = (200, 0)
|
|
180
|
+
math_pow.operation = 'POWER'
|
|
181
|
+
math_pow.inputs[1].default_value = 1.0 # Power of e (Euler's number)
|
|
182
|
+
|
|
183
|
+
# Multiply by 0.05 for final density - exact value from tutorial
|
|
184
|
+
math_mul2 = nodes.new(type='ShaderNodeMath')
|
|
185
|
+
math_mul2.location = (400, 0)
|
|
186
|
+
math_mul2.operation = 'MULTIPLY'
|
|
187
|
+
math_mul2.inputs[1].default_value = 0.05 # Tutorial uses exactly 0.05
|
|
188
|
+
|
|
189
|
+
# Displacement for atmosphere
|
|
190
|
+
displace = nodes.new(type='ShaderNodeDisplacement')
|
|
191
|
+
displace.location = (500, -200)
|
|
192
|
+
displace.inputs['Scale'].default_value = 1.0
|
|
193
|
+
|
|
194
|
+
# Connect nodes exactly as demonstrated in tutorial
|
|
195
|
+
links.new(tex_coord.outputs['Object'], vec_math.inputs[0])
|
|
196
|
+
links.new(vec_math.outputs[0], math_sub.inputs[0])
|
|
197
|
+
links.new(math_sub.outputs[0], math_div.inputs[0])
|
|
198
|
+
links.new(thickness.outputs[0], math_div.inputs[1])
|
|
199
|
+
links.new(math_div.outputs[0], math_mul1.inputs[0])
|
|
200
|
+
links.new(math_mul1.outputs[0], math_pow.inputs[0])
|
|
201
|
+
links.new(math_pow.outputs[0], math_mul2.inputs[0])
|
|
202
|
+
links.new(math_mul2.outputs[0], volume_scatter.inputs['Density'])
|
|
203
|
+
links.new(thickness.outputs[0], displace.inputs['Scale'])
|
|
204
|
+
links.new(volume_scatter.outputs['Volume'], output.inputs['Volume'])
|
|
205
|
+
links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
|
|
206
|
+
|
|
207
|
+
# Set material displacement method as in tutorial
|
|
208
|
+
atm_mat.displacement_method = 'DISPLACEMENT'
|
|
209
|
+
|
|
210
|
+
print("Atmosphere material created exactly as in tutorial")
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
return self.mcp.execute_code(generate_code())
|
|
214
|
+
|
|
215
|
+
def create_clouds_material(self, clouds_texture_name: str) -> Dict:
|
|
216
|
+
"""Create the clouds material with subsurface scattering."""
|
|
217
|
+
|
|
218
|
+
def generate_code():
|
|
219
|
+
return f"""
|
|
220
|
+
import bpy
|
|
221
|
+
|
|
222
|
+
# Create clouds material
|
|
223
|
+
clouds_mat = bpy.data.materials.new(name="clouds")
|
|
224
|
+
clouds_mat.use_nodes = True
|
|
225
|
+
|
|
226
|
+
# Get material nodes
|
|
227
|
+
nodes = clouds_mat.node_tree.nodes
|
|
228
|
+
links = clouds_mat.node_tree.links
|
|
229
|
+
|
|
230
|
+
# Clear default nodes
|
|
231
|
+
for node in nodes:
|
|
232
|
+
nodes.remove(node)
|
|
233
|
+
|
|
234
|
+
# Create nodes for clouds material - match tutorial exactly
|
|
235
|
+
output = nodes.new(type='ShaderNodeOutputMaterial')
|
|
236
|
+
output.location = (1000, 0)
|
|
237
|
+
|
|
238
|
+
# Add texture coordinate for clouds
|
|
239
|
+
tex_coord = nodes.new(type='ShaderNodeTexCoord')
|
|
240
|
+
tex_coord.location = (-900, 0)
|
|
241
|
+
|
|
242
|
+
# Add cloud texture
|
|
243
|
+
tex_clouds = nodes.new(type='ShaderNodeTexImage')
|
|
244
|
+
tex_clouds.location = (-600, 0)
|
|
245
|
+
tex_clouds.image = bpy.data.images.get("{clouds_texture_name}")
|
|
246
|
+
tex_clouds.projection = 'SPHERE'
|
|
247
|
+
tex_clouds.interpolation = 'Linear' # Match tutorial setting
|
|
248
|
+
|
|
249
|
+
# Gamma correction for cloud texture - exactly 0.5 as in tutorial
|
|
250
|
+
gamma = nodes.new(type='ShaderNodeGamma')
|
|
251
|
+
gamma.location = (-400, 0)
|
|
252
|
+
gamma.inputs['Gamma'].default_value = 0.5 # Exactly as in tutorial
|
|
253
|
+
|
|
254
|
+
# Second gamma correction - exactly 0.9 as in tutorial
|
|
255
|
+
gamma2 = nodes.new(type='ShaderNodeGamma')
|
|
256
|
+
gamma2.location = (-200, 0)
|
|
257
|
+
gamma2.inputs['Gamma'].default_value = 0.9 # Exactly as in tutorial
|
|
258
|
+
|
|
259
|
+
# Add Transparent BSDF
|
|
260
|
+
transparent = nodes.new(type='ShaderNodeBsdfTransparent')
|
|
261
|
+
transparent.location = (400, 100)
|
|
262
|
+
|
|
263
|
+
# Add Subsurface Scattering BSDF - exactly as in tutorial
|
|
264
|
+
subsurface = nodes.new(type='ShaderNodeSubsurfaceScattering')
|
|
265
|
+
subsurface.location = (400, -100)
|
|
266
|
+
subsurface.inputs['Radius'].default_value = (1, 1, 1) # Tutorial setting
|
|
267
|
+
|
|
268
|
+
# Multiply for cloud intensity - exactly 5.0 as in tutorial
|
|
269
|
+
math_mul = nodes.new(type='ShaderNodeMath')
|
|
270
|
+
math_mul.location = (0, -200)
|
|
271
|
+
math_mul.operation = 'MULTIPLY'
|
|
272
|
+
math_mul.inputs[1].default_value = 5.0 # Exactly as in tutorial
|
|
273
|
+
|
|
274
|
+
# Power for cloud exponential control - as in tutorial
|
|
275
|
+
math_pow = nodes.new(type='ShaderNodeMath')
|
|
276
|
+
math_pow.location = (200, -200)
|
|
277
|
+
math_pow.operation = 'POWER'
|
|
278
|
+
math_pow.inputs[1].default_value = 1.0 # Tutorial setting
|
|
279
|
+
|
|
280
|
+
# Mix shader
|
|
281
|
+
mix_shader = nodes.new(type='ShaderNodeMixShader')
|
|
282
|
+
mix_shader.location = (700, 0)
|
|
283
|
+
|
|
284
|
+
# Displacement node - exactly 0.005 as in tutorial
|
|
285
|
+
displace = nodes.new(type='ShaderNodeDisplacement')
|
|
286
|
+
displace.location = (700, -300)
|
|
287
|
+
displace.inputs['Scale'].default_value = 0.005 # Exactly as in tutorial
|
|
288
|
+
|
|
289
|
+
# Connect nodes exactly as in tutorial
|
|
290
|
+
links.new(tex_coord.outputs['Generated'], tex_clouds.inputs['Vector'])
|
|
291
|
+
links.new(tex_clouds.outputs['Color'], gamma.inputs['Color'])
|
|
292
|
+
links.new(gamma.outputs['Color'], gamma2.inputs['Color'])
|
|
293
|
+
links.new(gamma2.outputs['Color'], subsurface.inputs['Color'])
|
|
294
|
+
links.new(gamma2.outputs['Color'], math_mul.inputs[0])
|
|
295
|
+
links.new(math_mul.outputs[0], math_pow.inputs[0])
|
|
296
|
+
links.new(math_pow.outputs[0], mix_shader.inputs['Fac'])
|
|
297
|
+
links.new(transparent.outputs['BSDF'], mix_shader.inputs[1])
|
|
298
|
+
links.new(subsurface.outputs['BSSRDF'], mix_shader.inputs[2])
|
|
299
|
+
links.new(tex_clouds.outputs['Color'], displace.inputs['Height'])
|
|
300
|
+
links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])
|
|
301
|
+
links.new(displace.outputs['Displacement'], output.inputs['Displacement'])
|
|
302
|
+
|
|
303
|
+
# Set material displacement method as in tutorial
|
|
304
|
+
clouds_mat.displacement_method = 'BUMP' # Tutorial setting
|
|
305
|
+
|
|
306
|
+
print("Clouds material created exactly as in tutorial")
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
return self.mcp.execute_code(generate_code())
|
|
310
|
+
|
|
311
|
+
def set_material_color(self, object_name: str, color: tuple = (1, 0, 0, 1)) -> Dict:
|
|
312
|
+
"""
|
|
313
|
+
Set the material color for an object. Creates a new material if one doesn't exist.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
object_name: Name of the object to modify
|
|
317
|
+
color: RGBA color values as tuple (red, green, blue, alpha), values from 0-1
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Dictionary with the operation result
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def generate_code():
|
|
324
|
+
return f"""
|
|
325
|
+
import bpy
|
|
326
|
+
|
|
327
|
+
result = {{"status": "processing"}}
|
|
328
|
+
|
|
329
|
+
# Get the object
|
|
330
|
+
obj = bpy.data.objects.get("{object_name}")
|
|
331
|
+
if not obj:
|
|
332
|
+
result = {{"status": "error", "error": f"Object '{object_name}' not found"}}
|
|
333
|
+
else:
|
|
334
|
+
# Create a new material if needed
|
|
335
|
+
mat_name = "{object_name}_material"
|
|
336
|
+
if mat_name in bpy.data.materials:
|
|
337
|
+
mat = bpy.data.materials[mat_name]
|
|
338
|
+
else:
|
|
339
|
+
mat = bpy.data.materials.new(name=mat_name)
|
|
340
|
+
|
|
341
|
+
# Enable nodes for the material
|
|
342
|
+
mat.use_nodes = True
|
|
343
|
+
nodes = mat.node_tree.nodes
|
|
344
|
+
|
|
345
|
+
# Clear existing nodes
|
|
346
|
+
for node in nodes:
|
|
347
|
+
nodes.remove(node)
|
|
348
|
+
|
|
349
|
+
# Create new nodes
|
|
350
|
+
output = nodes.new(type='ShaderNodeOutputMaterial')
|
|
351
|
+
output.location = (300, 0)
|
|
352
|
+
|
|
353
|
+
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
354
|
+
bsdf.location = (0, 0)
|
|
355
|
+
|
|
356
|
+
# Set color - use correct format for Blender 4.0
|
|
357
|
+
bsdf.inputs["Base Color"].default_value = {color}
|
|
358
|
+
|
|
359
|
+
# Also set viewport display color for material
|
|
360
|
+
mat.diffuse_color = {color}
|
|
361
|
+
|
|
362
|
+
# Connect nodes
|
|
363
|
+
mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
|
|
364
|
+
|
|
365
|
+
# Assign material to object
|
|
366
|
+
if len(obj.data.materials) == 0:
|
|
367
|
+
obj.data.materials.append(mat)
|
|
368
|
+
else:
|
|
369
|
+
obj.data.materials[0] = mat
|
|
370
|
+
|
|
371
|
+
# Set the active material slot
|
|
372
|
+
obj.active_material_index = 0
|
|
373
|
+
obj.active_material = mat
|
|
374
|
+
|
|
375
|
+
# Set the render engine to show materials properly
|
|
376
|
+
# For Blender 4.0, use valid enum values
|
|
377
|
+
if hasattr(bpy.context.scene, 'render'):
|
|
378
|
+
if hasattr(bpy.context.scene.render, 'engine'):
|
|
379
|
+
current_engine = bpy.context.scene.render.engine
|
|
380
|
+
if current_engine not in ['CYCLES', 'BLENDER_EEVEE_NEXT', 'BLENDER_WORKBENCH']:
|
|
381
|
+
try:
|
|
382
|
+
bpy.context.scene.render.engine = 'CYCLES'
|
|
383
|
+
except Exception as e:
|
|
384
|
+
print(f"Could not set render engine: {{e}}")
|
|
385
|
+
|
|
386
|
+
# Force update of all 3D viewports
|
|
387
|
+
for window in bpy.context.window_manager.windows:
|
|
388
|
+
for area in window.screen.areas:
|
|
389
|
+
if area.type == 'VIEW_3D':
|
|
390
|
+
area.tag_redraw()
|
|
391
|
+
|
|
392
|
+
result = {{
|
|
393
|
+
"status": "success",
|
|
394
|
+
"message": "Material color set successfully",
|
|
395
|
+
"debug_info": {{
|
|
396
|
+
"object_name": obj.name,
|
|
397
|
+
"material_name": mat.name,
|
|
398
|
+
"color_set": list({color}),
|
|
399
|
+
"material_slots": len(obj.material_slots),
|
|
400
|
+
"active_material": obj.active_material.name if obj.active_material else None,
|
|
401
|
+
"render_engine": bpy.context.scene.render.engine if hasattr(bpy.context.scene, 'render') else "unknown"
|
|
402
|
+
}}
|
|
403
|
+
}}
|
|
404
|
+
|
|
405
|
+
result
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
return self.mcp.execute_code(generate_code())
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def generate_material_assignment_code(
|
|
412
|
+
object_name: str, material_name: str = None, color: tuple = (0.8, 0.8, 0.8, 1.0)
|
|
413
|
+
) -> str:
|
|
414
|
+
"""
|
|
415
|
+
Generates Python code to create a material and assign it to an object with proper error handling.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
object_name: Name of the object to assign material to
|
|
419
|
+
material_name: Name for the new material (default: derived from object name)
|
|
420
|
+
color: RGBA color tuple (r, g, b, a) with values from 0.0 to 1.0
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
String containing Python code that can be executed in Blender
|
|
424
|
+
"""
|
|
425
|
+
if material_name is None:
|
|
426
|
+
material_name = f"{object_name}_material"
|
|
427
|
+
|
|
428
|
+
# Build a code block with proper error handling
|
|
429
|
+
code = f"""
|
|
430
|
+
import bpy
|
|
431
|
+
|
|
432
|
+
result = {{"status": "processing"}}
|
|
433
|
+
|
|
434
|
+
# Get the object
|
|
435
|
+
obj = bpy.data.objects.get('{object_name}')
|
|
436
|
+
if not obj:
|
|
437
|
+
result = {{"status": "error", "message": "Object '{object_name}' not found"}}
|
|
438
|
+
else:
|
|
439
|
+
# Create the material if it doesn't exist
|
|
440
|
+
mat = bpy.data.materials.get('{material_name}')
|
|
441
|
+
if not mat:
|
|
442
|
+
mat = bpy.data.materials.new(name="{material_name}")
|
|
443
|
+
|
|
444
|
+
# Set the color
|
|
445
|
+
mat.diffuse_color = {color}
|
|
446
|
+
|
|
447
|
+
# Assign material to object
|
|
448
|
+
if len(obj.data.materials) == 0:
|
|
449
|
+
obj.data.materials.append(mat)
|
|
450
|
+
else:
|
|
451
|
+
obj.data.materials[0] = mat
|
|
452
|
+
|
|
453
|
+
result = {{
|
|
454
|
+
"status": "success",
|
|
455
|
+
"message": "Material created and assigned",
|
|
456
|
+
"object": "{object_name}",
|
|
457
|
+
"material": "{material_name}"
|
|
458
|
+
}}
|
|
459
|
+
|
|
460
|
+
result
|
|
461
|
+
"""
|
|
462
|
+
return code
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def generate_materials_for_all_objects_code() -> str:
|
|
466
|
+
"""
|
|
467
|
+
Generates Python code that creates default materials for all objects in the scene
|
|
468
|
+
that don't already have materials assigned.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
String containing Python code that can be executed in Blender
|
|
472
|
+
"""
|
|
473
|
+
code = """
|
|
474
|
+
import bpy
|
|
475
|
+
|
|
476
|
+
result = {"status": "processing", "created": 0, "objects": []}
|
|
477
|
+
|
|
478
|
+
# Process all objects in the scene
|
|
479
|
+
for obj in bpy.data.objects:
|
|
480
|
+
# Skip objects that can't have materials or already have materials
|
|
481
|
+
if obj.type not in {'MESH', 'CURVE', 'SURFACE', 'META', 'FONT'} or len(obj.material_slots) > 0 and obj.active_material:
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
# Create a new material for this object
|
|
485
|
+
mat_name = f"{obj.name}_material"
|
|
486
|
+
mat = bpy.data.materials.new(name=mat_name)
|
|
487
|
+
|
|
488
|
+
# Set default color based on object name (for consistent results)
|
|
489
|
+
import hashlib
|
|
490
|
+
name_hash = int(hashlib.md5(obj.name.encode()).hexdigest(), 16)
|
|
491
|
+
r = ((name_hash & 0xFF0000) >> 16) / 255.0
|
|
492
|
+
g = ((name_hash & 0x00FF00) >> 8) / 255.0
|
|
493
|
+
b = (name_hash & 0x0000FF) / 255.0
|
|
494
|
+
mat.diffuse_color = (r, g, b, 1.0)
|
|
495
|
+
|
|
496
|
+
# Assign the material to the object
|
|
497
|
+
obj.data.materials.append(mat)
|
|
498
|
+
|
|
499
|
+
result["objects"].append({"name": obj.name, "material": mat_name})
|
|
500
|
+
result["created"] += 1
|
|
501
|
+
|
|
502
|
+
result["status"] = "success"
|
|
503
|
+
result["message"] = f"Created materials for {result['created']} objects"
|
|
504
|
+
result
|
|
505
|
+
"""
|
|
506
|
+
return code
|