hdri and moidel import working
This commit is contained in:
parent
d9b2a6e68c
commit
2cd82ad93d
350
addon.py
350
addon.py
@ -3,7 +3,12 @@ import json
|
|||||||
import threading
|
import threading
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import requests # Add this import for HTTP requests
|
||||||
|
import tempfile # Add this import for temporary directories
|
||||||
from bpy.props import StringProperty, IntProperty
|
from bpy.props import StringProperty, IntProperty
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Blender MCP",
|
"name": "Blender MCP",
|
||||||
@ -134,7 +139,7 @@ class BlenderMCPServer:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error executing command: {str(e)}")
|
print(f"Error executing command: {str(e)}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@ -159,6 +164,10 @@ class BlenderMCPServer:
|
|||||||
"execute_code": self.execute_code,
|
"execute_code": self.execute_code,
|
||||||
"set_material": self.set_material,
|
"set_material": self.set_material,
|
||||||
"render_scene": self.render_scene,
|
"render_scene": self.render_scene,
|
||||||
|
# Add Polyhaven handlers
|
||||||
|
"get_polyhaven_categories": self.get_polyhaven_categories,
|
||||||
|
"search_polyhaven_assets": self.search_polyhaven_assets,
|
||||||
|
"download_polyhaven_asset": self.download_polyhaven_asset,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler = handlers.get(cmd_type)
|
handler = handlers.get(cmd_type)
|
||||||
@ -170,7 +179,6 @@ class BlenderMCPServer:
|
|||||||
return {"status": "success", "result": result}
|
return {"status": "success", "result": result}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in handler: {str(e)}")
|
print(f"Error in handler: {str(e)}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
else:
|
else:
|
||||||
@ -216,7 +224,6 @@ class BlenderMCPServer:
|
|||||||
return scene_info
|
return scene_info
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in get_scene_info: {str(e)}")
|
print(f"Error in get_scene_info: {str(e)}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@ -427,7 +434,6 @@ class BlenderMCPServer:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in set_material: {str(e)}")
|
print(f"Error in set_material: {str(e)}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
@ -456,6 +462,342 @@ class BlenderMCPServer:
|
|||||||
"resolution": [bpy.context.scene.render.resolution_x, bpy.context.scene.render.resolution_y],
|
"resolution": [bpy.context.scene.render.resolution_x, bpy.context.scene.render.resolution_y],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_polyhaven_categories(self, asset_type):
|
||||||
|
"""Get categories for a specific asset type from Polyhaven"""
|
||||||
|
try:
|
||||||
|
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}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"categories": response.json()}
|
||||||
|
else:
|
||||||
|
return {"error": f"API request failed with status code {response.status_code}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def search_polyhaven_assets(self, asset_type=None, categories=None):
|
||||||
|
"""Search for assets from Polyhaven with optional filtering"""
|
||||||
|
try:
|
||||||
|
url = "https://api.polyhaven.com/assets"
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if asset_type and asset_type != "all":
|
||||||
|
if asset_type not in ["hdris", "textures", "models"]:
|
||||||
|
return {"error": f"Invalid asset type: {asset_type}. Must be one of: hdris, textures, models, all"}
|
||||||
|
params["type"] = asset_type
|
||||||
|
|
||||||
|
if categories:
|
||||||
|
params["categories"] = categories
|
||||||
|
|
||||||
|
response = requests.get(url, params=params)
|
||||||
|
if response.status_code == 200:
|
||||||
|
# Limit the response size to avoid overwhelming Blender
|
||||||
|
assets = response.json()
|
||||||
|
# Return only the first 20 assets to keep response size manageable
|
||||||
|
limited_assets = {}
|
||||||
|
for i, (key, value) in enumerate(assets.items()):
|
||||||
|
if i >= 20: # Limit to 20 assets
|
||||||
|
break
|
||||||
|
limited_assets[key] = value
|
||||||
|
|
||||||
|
return {"assets": limited_assets, "total_count": len(assets), "returned_count": len(limited_assets)}
|
||||||
|
else:
|
||||||
|
return {"error": f"API request failed with status code {response.status_code}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_format=None):
|
||||||
|
"""Download an asset from Polyhaven and import it into Blender"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# First get the files information
|
||||||
|
files_response = requests.get(f"https://api.polyhaven.com/files/{asset_id}")
|
||||||
|
if files_response.status_code != 200:
|
||||||
|
return {"error": f"Failed to get asset files: {files_response.status_code}"}
|
||||||
|
|
||||||
|
files_data = files_response.json()
|
||||||
|
|
||||||
|
# Handle different asset types
|
||||||
|
if asset_type == "hdris":
|
||||||
|
# For HDRIs, download the .hdr or .exr file
|
||||||
|
if not file_format:
|
||||||
|
file_format = "hdr" # Default format for HDRIs
|
||||||
|
|
||||||
|
if "hdri" in files_data and resolution in files_data["hdri"] and file_format in files_data["hdri"][resolution]:
|
||||||
|
file_info = files_data["hdri"][resolution][file_format]
|
||||||
|
file_url = file_info["url"]
|
||||||
|
|
||||||
|
# For HDRIs, we need to save to a temporary file first
|
||||||
|
# since Blender can't properly load HDR data directly from memory
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=f".{file_format}", delete=False) as tmp_file:
|
||||||
|
# Download the file
|
||||||
|
response = requests.get(file_url)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return {"error": f"Failed to download HDRI: {response.status_code}"}
|
||||||
|
|
||||||
|
tmp_file.write(response.content)
|
||||||
|
tmp_path = tmp_file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create a new world if none exists
|
||||||
|
if not bpy.data.worlds:
|
||||||
|
bpy.data.worlds.new("World")
|
||||||
|
|
||||||
|
world = bpy.data.worlds[0]
|
||||||
|
world.use_nodes = True
|
||||||
|
node_tree = world.node_tree
|
||||||
|
|
||||||
|
# Clear existing nodes
|
||||||
|
for node in node_tree.nodes:
|
||||||
|
node_tree.nodes.remove(node)
|
||||||
|
|
||||||
|
# Create nodes
|
||||||
|
tex_coord = node_tree.nodes.new(type='ShaderNodeTexCoord')
|
||||||
|
tex_coord.location = (-800, 0)
|
||||||
|
|
||||||
|
mapping = node_tree.nodes.new(type='ShaderNodeMapping')
|
||||||
|
mapping.location = (-600, 0)
|
||||||
|
|
||||||
|
# Load the image from the temporary file
|
||||||
|
env_tex = node_tree.nodes.new(type='ShaderNodeTexEnvironment')
|
||||||
|
env_tex.location = (-400, 0)
|
||||||
|
env_tex.image = bpy.data.images.load(tmp_path)
|
||||||
|
|
||||||
|
# FIXED: Use a color space that exists in all Blender versions
|
||||||
|
if file_format.lower() == 'exr':
|
||||||
|
# Try to use Linear color space for EXR files
|
||||||
|
try:
|
||||||
|
env_tex.image.colorspace_settings.name = 'Linear'
|
||||||
|
except:
|
||||||
|
# Fallback to Non-Color if Linear isn't available
|
||||||
|
env_tex.image.colorspace_settings.name = 'Non-Color'
|
||||||
|
else: # hdr
|
||||||
|
# For HDR files, try these options in order
|
||||||
|
for color_space in ['Linear', 'Linear Rec.709', 'Non-Color']:
|
||||||
|
try:
|
||||||
|
env_tex.image.colorspace_settings.name = color_space
|
||||||
|
break # Stop if we successfully set a color space
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
background = node_tree.nodes.new(type='ShaderNodeBackground')
|
||||||
|
background.location = (-200, 0)
|
||||||
|
|
||||||
|
output = node_tree.nodes.new(type='ShaderNodeOutputWorld')
|
||||||
|
output.location = (0, 0)
|
||||||
|
|
||||||
|
# Connect nodes
|
||||||
|
node_tree.links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])
|
||||||
|
node_tree.links.new(mapping.outputs['Vector'], env_tex.inputs['Vector'])
|
||||||
|
node_tree.links.new(env_tex.outputs['Color'], background.inputs['Color'])
|
||||||
|
node_tree.links.new(background.outputs['Background'], output.inputs['Surface'])
|
||||||
|
|
||||||
|
# Set as active world
|
||||||
|
bpy.context.scene.world = world
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
try:
|
||||||
|
tempfile._cleanup() # This will clean up all temporary files
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"HDRI {asset_id} imported successfully",
|
||||||
|
"image_name": env_tex.image.name
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": f"Failed to set up HDRI in Blender: {str(e)}"}
|
||||||
|
else:
|
||||||
|
return {"error": f"Requested resolution or format not available for this HDRI"}
|
||||||
|
|
||||||
|
elif asset_type == "textures":
|
||||||
|
# For textures, download available maps
|
||||||
|
if not file_format:
|
||||||
|
file_format = "jpg" # Default format for textures
|
||||||
|
|
||||||
|
# Find available maps (diffuse, normal, etc.)
|
||||||
|
downloaded_maps = {}
|
||||||
|
for map_type in files_data:
|
||||||
|
if map_type not in ["blend", "gltf"]: # Skip non-texture files
|
||||||
|
if resolution in files_data[map_type] and file_format in files_data[map_type][resolution]:
|
||||||
|
file_info = files_data[map_type][resolution][file_format]
|
||||||
|
file_url = file_info["url"]
|
||||||
|
|
||||||
|
# Download the file directly into Blender's memory
|
||||||
|
response = requests.get(file_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
# Create a new image in Blender's memory
|
||||||
|
image_name = f"{asset_id}_{map_type}.{file_format}"
|
||||||
|
image = bpy.data.images.new(name=image_name, width=1, height=1)
|
||||||
|
|
||||||
|
# Save the downloaded data
|
||||||
|
image.file_format = file_format.upper()
|
||||||
|
image.filepath_raw = f"/tmp/{image_name}" # This is just for reference
|
||||||
|
image.pack(data=response.content)
|
||||||
|
|
||||||
|
downloaded_maps[map_type] = image
|
||||||
|
|
||||||
|
if not downloaded_maps:
|
||||||
|
return {"error": f"No texture maps found for the requested resolution and format"}
|
||||||
|
|
||||||
|
# Create a new material with the downloaded textures
|
||||||
|
mat = bpy.data.materials.new(name=asset_id)
|
||||||
|
mat.use_nodes = True
|
||||||
|
nodes = mat.node_tree.nodes
|
||||||
|
links = mat.node_tree.links
|
||||||
|
|
||||||
|
# Clear default nodes
|
||||||
|
for node in nodes:
|
||||||
|
nodes.remove(node)
|
||||||
|
|
||||||
|
# Create output node
|
||||||
|
output = nodes.new(type='ShaderNodeOutputMaterial')
|
||||||
|
output.location = (300, 0)
|
||||||
|
|
||||||
|
# Create principled BSDF node
|
||||||
|
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
|
||||||
|
principled.location = (0, 0)
|
||||||
|
links.new(principled.outputs[0], output.inputs[0])
|
||||||
|
|
||||||
|
# Add texture nodes based on available maps
|
||||||
|
tex_coord = nodes.new(type='ShaderNodeTexCoord')
|
||||||
|
tex_coord.location = (-800, 0)
|
||||||
|
|
||||||
|
mapping = nodes.new(type='ShaderNodeMapping')
|
||||||
|
mapping.location = (-600, 0)
|
||||||
|
links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])
|
||||||
|
|
||||||
|
# Position offset for texture nodes
|
||||||
|
x_pos = -400
|
||||||
|
y_pos = 300
|
||||||
|
|
||||||
|
# Connect different texture maps
|
||||||
|
for map_type, image in downloaded_maps.items():
|
||||||
|
tex_node = nodes.new(type='ShaderNodeTexImage')
|
||||||
|
tex_node.location = (x_pos, y_pos)
|
||||||
|
tex_node.image = image
|
||||||
|
tex_node.image.colorspace_settings.name = 'sRGB' if map_type in ['color', 'diffuse', 'albedo'] else 'Non-Color'
|
||||||
|
|
||||||
|
links.new(mapping.outputs['Vector'], tex_node.inputs['Vector'])
|
||||||
|
|
||||||
|
# Connect to appropriate input on Principled BSDF
|
||||||
|
if map_type in ['color', 'diffuse', 'albedo']:
|
||||||
|
links.new(tex_node.outputs['Color'], principled.inputs['Base Color'])
|
||||||
|
elif map_type in ['roughness', 'rough']:
|
||||||
|
links.new(tex_node.outputs['Color'], principled.inputs['Roughness'])
|
||||||
|
elif map_type in ['metallic', 'metalness', 'metal']:
|
||||||
|
links.new(tex_node.outputs['Color'], principled.inputs['Metallic'])
|
||||||
|
elif map_type in ['normal', 'nor']:
|
||||||
|
# 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 in ['displacement', 'disp', 'height']:
|
||||||
|
# Add displacement node
|
||||||
|
disp_node = nodes.new(type='ShaderNodeDisplacement')
|
||||||
|
disp_node.location = (x_pos + 200, y_pos - 200)
|
||||||
|
links.new(tex_node.outputs['Color'], disp_node.inputs['Height'])
|
||||||
|
links.new(disp_node.outputs['Displacement'], output.inputs['Displacement'])
|
||||||
|
|
||||||
|
y_pos -= 250
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Texture {asset_id} imported as material",
|
||||||
|
"material": mat.name,
|
||||||
|
"maps": list(downloaded_maps.keys())
|
||||||
|
}
|
||||||
|
|
||||||
|
elif asset_type == "models":
|
||||||
|
# For models, prefer glTF format if available
|
||||||
|
if not file_format:
|
||||||
|
file_format = "gltf" # Default format for models
|
||||||
|
|
||||||
|
if file_format in files_data and resolution in files_data[file_format]:
|
||||||
|
file_info = files_data[file_format][resolution][file_format]
|
||||||
|
file_url = file_info["url"]
|
||||||
|
|
||||||
|
# Create a temporary directory to store the model and its dependencies
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
main_file_path = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Download the main model file
|
||||||
|
main_file_name = file_url.split("/")[-1]
|
||||||
|
main_file_path = os.path.join(temp_dir, main_file_name)
|
||||||
|
|
||||||
|
response = requests.get(file_url)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return {"error": f"Failed to download model: {response.status_code}"}
|
||||||
|
|
||||||
|
with open(main_file_path, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
# Check for included files and download them
|
||||||
|
if "include" in file_info and file_info["include"]:
|
||||||
|
for include_path, include_info in file_info["include"].items():
|
||||||
|
# Get the URL for the included file - this is the fix
|
||||||
|
include_url = include_info["url"]
|
||||||
|
|
||||||
|
# Create the directory structure for the included file
|
||||||
|
include_file_path = os.path.join(temp_dir, include_path)
|
||||||
|
os.makedirs(os.path.dirname(include_file_path), exist_ok=True)
|
||||||
|
|
||||||
|
# Download the included file
|
||||||
|
include_response = requests.get(include_url)
|
||||||
|
if include_response.status_code == 200:
|
||||||
|
with open(include_file_path, "wb") as f:
|
||||||
|
f.write(include_response.content)
|
||||||
|
else:
|
||||||
|
print(f"Failed to download included file: {include_path}")
|
||||||
|
|
||||||
|
# Import the model into Blender
|
||||||
|
if file_format == "gltf" or file_format == "glb":
|
||||||
|
bpy.ops.import_scene.gltf(filepath=main_file_path)
|
||||||
|
elif file_format == "fbx":
|
||||||
|
bpy.ops.import_scene.fbx(filepath=main_file_path)
|
||||||
|
elif file_format == "obj":
|
||||||
|
bpy.ops.import_scene.obj(filepath=main_file_path)
|
||||||
|
elif file_format == "blend":
|
||||||
|
# For blend files, we need to append or link
|
||||||
|
with bpy.data.libraries.load(main_file_path, link=False) as (data_from, data_to):
|
||||||
|
data_to.objects = data_from.objects
|
||||||
|
|
||||||
|
# Link the objects to the scene
|
||||||
|
for obj in data_to.objects:
|
||||||
|
if obj is not None:
|
||||||
|
bpy.context.collection.objects.link(obj)
|
||||||
|
else:
|
||||||
|
return {"error": f"Unsupported model format: {file_format}"}
|
||||||
|
|
||||||
|
# Get the names of imported objects
|
||||||
|
imported_objects = [obj.name for obj in bpy.context.selected_objects]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Model {asset_id} imported successfully",
|
||||||
|
"imported_objects": imported_objects
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": f"Failed to import model: {str(e)}"}
|
||||||
|
finally:
|
||||||
|
# Clean up temporary directory
|
||||||
|
try:
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
except:
|
||||||
|
print(f"Failed to clean up temporary directory: {temp_dir}")
|
||||||
|
else:
|
||||||
|
return {"error": f"Requested format or resolution not available for this model"}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {"error": f"Unsupported asset type: {asset_type}"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": f"Failed to download asset: {str(e)}"}
|
||||||
|
|
||||||
# Blender UI Panel
|
# Blender UI Panel
|
||||||
class BLENDERMCP_PT_Panel(bpy.types.Panel):
|
class BLENDERMCP_PT_Panel(bpy.types.Panel):
|
||||||
bl_label = "Blender MCP"
|
bl_label = "Blender MCP"
|
||||||
|
|||||||
@ -263,41 +263,7 @@ def get_object_info(ctx: Context, object_name: str) -> str:
|
|||||||
# Tool endpoints
|
# Tool endpoints
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def create_primitive(
|
|
||||||
ctx: Context,
|
|
||||||
type: str = "CUBE",
|
|
||||||
location: List[float] = None,
|
|
||||||
color: List[float] = None
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Create a basic primitive object in Blender.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- type: Object type (CUBE, SPHERE, CYLINDER, PLANE)
|
|
||||||
- location: Optional [x, y, z] location coordinates
|
|
||||||
- color: Optional [R, G, B] color values (0.0-1.0)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
blender = get_blender_connection()
|
|
||||||
loc = location or [0, 0, 0]
|
|
||||||
|
|
||||||
# First create the object
|
|
||||||
params = {
|
|
||||||
"type": type,
|
|
||||||
"location": loc
|
|
||||||
}
|
|
||||||
result = blender.send_command("create_object", params)
|
|
||||||
|
|
||||||
# If color specified, set the material
|
|
||||||
if color:
|
|
||||||
blender.send_command("set_material", {
|
|
||||||
"object_name": result["name"],
|
|
||||||
"color": color
|
|
||||||
})
|
|
||||||
|
|
||||||
return f"Created {type} at location {loc}"
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error creating primitive: {str(e)}"
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def set_object_property(
|
def set_object_property(
|
||||||
@ -474,15 +440,137 @@ def execute_blender_code(ctx: Context, code: str) -> str:
|
|||||||
logger.error(f"Error executing code: {str(e)}")
|
logger.error(f"Error executing code: {str(e)}")
|
||||||
return f"Error executing code: {str(e)}"
|
return f"Error executing code: {str(e)}"
|
||||||
|
|
||||||
@mcp.prompt()
|
@mcp.tool()
|
||||||
def create_basic_object() -> str:
|
def get_polyhaven_categories(ctx: Context, asset_type: str = "hdris") -> str:
|
||||||
"""Create a single object with basic properties"""
|
"""
|
||||||
return """Create a blue cube at position [0, 1, 0]"""
|
Get a list of categories for a specific asset type on Polyhaven.
|
||||||
|
|
||||||
@mcp.prompt()
|
Parameters:
|
||||||
def modify_basic_object() -> str:
|
- asset_type: The type of asset to get categories for (hdris, textures, models, all)
|
||||||
"""Modify a single property of an object"""
|
|
||||||
return """Make the cube red"""
|
Returns a list of categories with the count of assets in each category.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
blender = get_blender_connection()
|
||||||
|
result = blender.send_command("get_polyhaven_categories", {"asset_type": asset_type})
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
return f"Error: {result['error']}"
|
||||||
|
|
||||||
|
# Format the categories in a more readable way
|
||||||
|
categories = result["categories"]
|
||||||
|
formatted_output = f"Categories for {asset_type}:\n\n"
|
||||||
|
|
||||||
|
# Sort categories by count (descending)
|
||||||
|
sorted_categories = sorted(categories.items(), key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
for category, count in sorted_categories:
|
||||||
|
formatted_output += f"- {category}: {count} assets\n"
|
||||||
|
|
||||||
|
return formatted_output
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting Polyhaven categories: {str(e)}")
|
||||||
|
return f"Error getting Polyhaven categories: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def search_polyhaven_assets(
|
||||||
|
ctx: Context,
|
||||||
|
asset_type: str = "all",
|
||||||
|
categories: str = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Search for assets on Polyhaven with optional filtering.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- asset_type: Type of assets to search for (hdris, textures, models, all)
|
||||||
|
- categories: Optional comma-separated list of categories to filter by
|
||||||
|
|
||||||
|
Returns a list of matching assets with basic information.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
blender = get_blender_connection()
|
||||||
|
result = blender.send_command("search_polyhaven_assets", {
|
||||||
|
"asset_type": asset_type,
|
||||||
|
"categories": categories
|
||||||
|
})
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
return f"Error: {result['error']}"
|
||||||
|
|
||||||
|
# Format the assets in a more readable way
|
||||||
|
assets = result["assets"]
|
||||||
|
total_count = result["total_count"]
|
||||||
|
returned_count = result["returned_count"]
|
||||||
|
|
||||||
|
formatted_output = f"Found {total_count} assets"
|
||||||
|
if categories:
|
||||||
|
formatted_output += f" in categories: {categories}"
|
||||||
|
formatted_output += f"\nShowing {returned_count} assets:\n\n"
|
||||||
|
|
||||||
|
# Sort assets by download count (popularity)
|
||||||
|
sorted_assets = sorted(assets.items(), key=lambda x: x[1].get("download_count", 0), reverse=True)
|
||||||
|
|
||||||
|
for asset_id, asset_data in sorted_assets:
|
||||||
|
formatted_output += f"- {asset_data.get('name', asset_id)} (ID: {asset_id})\n"
|
||||||
|
formatted_output += f" Type: {['HDRI', 'Texture', 'Model'][asset_data.get('type', 0)]}\n"
|
||||||
|
formatted_output += f" Categories: {', '.join(asset_data.get('categories', []))}\n"
|
||||||
|
formatted_output += f" Downloads: {asset_data.get('download_count', 'Unknown')}\n\n"
|
||||||
|
|
||||||
|
return formatted_output
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error searching Polyhaven assets: {str(e)}")
|
||||||
|
return f"Error searching Polyhaven assets: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def download_polyhaven_asset(
|
||||||
|
ctx: Context,
|
||||||
|
asset_id: str,
|
||||||
|
asset_type: str,
|
||||||
|
resolution: str = "1k",
|
||||||
|
file_format: str = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Download and import a Polyhaven asset into Blender.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- asset_id: The ID of the asset to download
|
||||||
|
- asset_type: The type of asset (hdris, textures, models)
|
||||||
|
- resolution: The resolution to download (e.g., 1k, 2k, 4k)
|
||||||
|
- file_format: Optional file format (e.g., hdr, exr for HDRIs; jpg, png for textures; gltf, fbx for models)
|
||||||
|
|
||||||
|
Returns a message indicating success or failure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
blender = get_blender_connection()
|
||||||
|
result = blender.send_command("download_polyhaven_asset", {
|
||||||
|
"asset_id": asset_id,
|
||||||
|
"asset_type": asset_type,
|
||||||
|
"resolution": resolution,
|
||||||
|
"file_format": file_format
|
||||||
|
})
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
return f"Error: {result['error']}"
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
message = result.get("message", "Asset downloaded and imported successfully")
|
||||||
|
|
||||||
|
# Add additional information based on asset type
|
||||||
|
if asset_type == "hdris":
|
||||||
|
return f"{message}. The HDRI has been set as the world environment."
|
||||||
|
elif asset_type == "textures":
|
||||||
|
material_name = result.get("material", "")
|
||||||
|
maps = ", ".join(result.get("maps", []))
|
||||||
|
return f"{message}. Created material '{material_name}' with maps: {maps}."
|
||||||
|
elif asset_type == "models":
|
||||||
|
return f"{message}. The model has been imported into the current scene."
|
||||||
|
else:
|
||||||
|
return message
|
||||||
|
else:
|
||||||
|
return f"Failed to download asset: {result.get('message', 'Unknown error')}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error downloading Polyhaven asset: {str(e)}")
|
||||||
|
return f"Error downloading Polyhaven asset: {str(e)}"
|
||||||
|
|
||||||
# Main execution
|
# Main execution
|
||||||
|
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@ -28,7 +28,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blender-mcp"
|
name = "blender-mcp"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "mcp", extra = ["cli"] },
|
{ name = "mcp", extra = ["cli"] },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user