Feat add: Added Hyper3D support.

This commit is contained in:
ElgoogUdiab 2025-03-16 15:50:15 +08:00
parent 430821326d
commit cb2b825a30
2 changed files with 589 additions and 9 deletions

356
addon.py
View File

@ -163,6 +163,7 @@ class BlenderMCPServer:
"execute_code": self.execute_code,
"set_material": self.set_material,
"get_polyhaven_status": self.get_polyhaven_status,
"get_hyper3d_status": self.get_hyper3d_status,
}
# Add Polyhaven handlers only if enabled
@ -175,6 +176,15 @@ class BlenderMCPServer:
}
handlers.update(polyhaven_handlers)
# Add Hyper3d handlers only if enabled
if bpy.context.scene.blendermcp_use_hyper3d:
polyhaven_handlers = {
"create_rodin_job": self.create_rodin_job,
"poll_rodin_job_status": self.poll_rodin_job_status,
"import_generated_asset": self.import_generated_asset,
}
handlers.update(polyhaven_handlers)
handler = handlers.get(cmd_type)
if handler:
try:
@ -1204,6 +1214,322 @@ class BlenderMCPServer:
3. Restart the connection to Claude"""
}
#region Hyper3D
def get_hyper3d_status(self):
"""Get the current status of Hyper3D Rodin integration"""
enabled = bpy.context.scene.blendermcp_use_hyper3d
if enabled:
if not bpy.context.scene.blendermcp_hyper3d_api_key:
return {
"enabled": False,
"message": """Hyper3D Rodin integration is currently enabled, but API key is not given. To enable it:
1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)
2. Keep the 'Use Hyper3D Rodin 3D model generation' checkbox checked
3. Choose the right plaform and fill in the API Key
4. Restart the connection to Claude"""
}
mode = bpy.context.scene.blendermcp_hyper3d_mode
message = f"Hyper3D Rodin integration is enabled and ready to use. Mode: {mode}"
return {
"enabled": True,
"message": message
}
else:
return {
"enabled": False,
"message": """Hyper3D Rodin integration is currently disabled. To enable it:
1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)
2. Check the 'Use Hyper3D Rodin 3D model generation' checkbox
3. Restart the connection to Claude"""
}
def create_rodin_job(self, *args, **kwargs):
match bpy.context.scene.blendermcp_hyper3d_mode:
case "MAIN_SITE":
return self.create_rodin_job_main_site(*args, **kwargs)
case "FAL_AI":
return self.create_rodin_job_fal_ai(*args, **kwargs)
case _:
return f"Error: Unknown Hyper3D Rodin mode!"
def create_rodin_job_main_site(
self,
text_prompt: str=None,
images: list[tuple[str, str]]=None,
bbox_condition=None
):
try:
if images is None:
images = []
"""Call Rodin API, get the job uuid and subscription key"""
files = [
*[("images", (f"{i:04d}{img_suffix}", img)) for i, (img_suffix, img) in enumerate(images)],
("tier", (None, "Sketch")),
("mesh_mode", (None, "Raw")),
]
if text_prompt:
files.append(("prompt", (None, text_prompt)))
if bbox_condition:
files.append(("bbox_condition", (None, json.dumps(bbox_condition))))
response = requests.post(
"https://hyperhuman.deemos.com/api/v2/rodin",
headers={
"Authorization": f"Bearer {bpy.context.scene.blendermcp_hyper3d_api_key}",
},
files=files
)
data = response.json()
return data
except Exception as e:
return {"error": str(e)}
def create_rodin_job_fal_ai(
self,
text_prompt: str=None,
images: list[tuple[str, str]]=None,
bbox_condition=None
):
try:
req_data = {
"tier": "Sketch",
}
if images:
req_data["input_image_urls"] = images
if text_prompt:
req_data["prompt"] = text_prompt
if bbox_condition:
req_data["bbox_condition"] = bbox_condition
response = requests.post(
"https://queue.fal.run/fal-ai/hyper3d/rodin",
headers={
"Authorization": f"Key {bpy.context.scene.blendermcp_hyper3d_api_key}",
"Content-Type": "application/json",
},
json=req_data
)
data = response.json()
return data
except Exception as e:
return {"error": str(e)}
def poll_rodin_job_status(self, *args, **kwargs):
match bpy.context.scene.blendermcp_hyper3d_mode:
case "MAIN_SITE":
return self.poll_rodin_job_status_main_site(*args, **kwargs)
case "FAL_AI":
return self.poll_rodin_job_status_fal_ai(*args, **kwargs)
case _:
return f"Error: Unknown Hyper3D Rodin mode!"
def poll_rodin_job_status_main_site(self, subscription_key: str):
"""Call the job status API to get the job status"""
response = requests.post(
"https://hyperhuman.deemos.com/api/v2/status",
headers={
"Authorization": f"Bearer {bpy.context.scene.blendermcp_hyper3d_api_key}",
},
json={
"subscription_key": subscription_key,
},
)
data = response.json()
return {
"status_list": [i["status"] for i in data["jobs"]]
}
def poll_rodin_job_status_fal_ai(self, request_id: str):
"""Call the job status API to get the job status"""
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}",
},
)
data = response.json()
return data
@staticmethod
def _clean_imported_glb(filepath, mesh_name=None):
# Import the GLB file
bpy.ops.import_scene.gltf(filepath=filepath)
# Ensure the context is updated
bpy.context.view_layer.update()
# Get all imported objects
imported_objects = [obj for obj in bpy.context.view_layer.objects if obj.select_get()]
if not imported_objects:
print("Error: No objects were imported.")
return
# Identify the mesh object
mesh_obj = None
if len(imported_objects) == 1 and imported_objects[0].type == 'MESH':
mesh_obj = imported_objects[0]
print("Single mesh imported, no cleanup needed.")
else:
parent_obj = imported_objects[0]
if parent_obj.type == 'EMPTY' and len(parent_obj.children) == 1:
potential_mesh = parent_obj.children[0]
if potential_mesh.type == 'MESH':
print("GLB structure confirmed: Empty node with one mesh child.")
# Unparent the mesh from the empty node
potential_mesh.parent = None
# Remove the empty node
bpy.data.objects.remove(parent_obj)
print("Removed empty node, keeping only the mesh.")
mesh_obj = potential_mesh
else:
print("Error: Child is not a mesh object.")
return
else:
print("Error: Expected an empty node with one mesh child or a single mesh object.")
return
# Rename the mesh if needed
if mesh_obj and mesh_name:
mesh_obj.name = mesh_name
print(f"Mesh renamed to: {mesh_name}")
return mesh_obj
def import_generated_asset(self, *args, **kwargs):
match bpy.context.scene.blendermcp_hyper3d_mode:
case "MAIN_SITE":
return self.import_generated_asset_main_site(*args, **kwargs)
case "FAL_AI":
return self.import_generated_asset_fal_ai(*args, **kwargs)
case _:
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"""
response = requests.post(
"https://hyperhuman.deemos.com/api/v2/download",
headers={
"Authorization": f"Bearer {bpy.context.scene.blendermcp_hyper3d_api_key}",
},
json={
'task_uuid': task_uuid
}
)
data_ = response.json()
temp_file = None
for i in data_["list"]:
if i["name"].endswith(".glb"):
temp_file = tempfile.NamedTemporaryFile(
delete=False,
prefix=task_uuid,
suffix=".glb",
)
try:
# Download the content
response = requests.get(i["url"], stream=True)
response.raise_for_status() # Raise an exception for HTTP errors
# Write the content to the temporary file
for chunk in response.iter_content(chunk_size=8192):
temp_file.write(chunk)
# Close the file
temp_file.close()
except Exception as e:
# Clean up the file if there's an error
temp_file.close()
os.unlink(temp_file.name)
return {"succeed": False, "error": str(e)}
break
try:
obj = self._clean_imported_glb(
filepath=temp_file.name,
mesh_name=name
)
result = {
"name": obj.name,
"type": obj.type,
"location": [obj.location.x, obj.location.y, obj.location.z],
"rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z],
"scale": [obj.scale.x, obj.scale.y, obj.scale.z],
}
if obj.type == "MESH":
bounding_box = self._get_aabb(obj)
result["world_bounding_box"] = bounding_box
return {
"succeed": True, **result
}
except Exception as e:
return {"succeed": False, "error": str(e)}
def import_generated_asset_fal_ai(self, request_id: str, name: str):
"""Fetch the generated asset, import into blender"""
response = requests.get(
f"https://queue.fal.run/fal-ai/hyper3d/requests/{request_id}",
headers={
"Authorization": f"Key {bpy.context.scene.blendermcp_hyper3d_api_key}",
}
)
data_ = response.json()
temp_file = None
temp_file = tempfile.NamedTemporaryFile(
delete=False,
prefix=request_id,
suffix=".glb",
)
try:
# Download the content
response = requests.get(data_["model_mesh"]["url"], stream=True)
response.raise_for_status() # Raise an exception for HTTP errors
# Write the content to the temporary file
for chunk in response.iter_content(chunk_size=8192):
temp_file.write(chunk)
# Close the file
temp_file.close()
except Exception as e:
# Clean up the file if there's an error
temp_file.close()
os.unlink(temp_file.name)
return {"succeed": False, "error": str(e)}
try:
obj = self._clean_imported_glb(
filepath=temp_file.name,
mesh_name=name
)
result = {
"name": obj.name,
"type": obj.type,
"location": [obj.location.x, obj.location.y, obj.location.z],
"rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z],
"scale": [obj.scale.x, obj.scale.y, obj.scale.z],
}
if obj.type == "MESH":
bounding_box = self._get_aabb(obj)
result["world_bounding_box"] = bounding_box
return {
"succeed": True, **result
}
except Exception as e:
return {"succeed": False, "error": str(e)}
#endregion
# Blender UI Panel
class BLENDERMCP_PT_Panel(bpy.types.Panel):
bl_label = "Blender MCP"
@ -1219,6 +1545,11 @@ class BLENDERMCP_PT_Panel(bpy.types.Panel):
layout.prop(scene, "blendermcp_port")
layout.prop(scene, "blendermcp_use_polyhaven", text="Use assets from Poly Haven")
layout.prop(scene, "blendermcp_use_hyper3d", text="Use Hyper3D Rodin 3D model generation")
if scene.blendermcp_use_hyper3d:
layout.prop(scene, "blendermcp_hyper3d_mode", text="Rodin Mode")
layout.prop(scene, "blendermcp_hyper3d_api_key", text="API Key")
if not scene.blendermcp_server_running:
layout.operator("blendermcp.start_server", text="Start MCP Server")
else:
@ -1283,6 +1614,28 @@ def register():
default=False
)
bpy.types.Scene.blendermcp_use_hyper3d = bpy.props.BoolProperty(
name="Use Hyper3D Rodin",
description="Enable Hyper3D Rodin generatino integration",
default=False
)
bpy.types.Scene.blendermcp_hyper3d_mode = bpy.props.EnumProperty(
name="Rodin Mode",
description="Choose the platform used to call Rodin APIs",
items=[
("MAIN_SITE", "hyper3d.ai", "hyper3d.ai"),
("FAL_AI", "fal.ai", "fal.ai"),
],
default="MAIN_SITE"
)
bpy.types.Scene.blendermcp_hyper3d_api_key = bpy.props.StringProperty(
name="Hyper3D API Key",
description="API Key provided by Hyper3D",
default=""
)
bpy.utils.register_class(BLENDERMCP_PT_Panel)
bpy.utils.register_class(BLENDERMCP_OT_StartServer)
bpy.utils.register_class(BLENDERMCP_OT_StopServer)
@ -1302,6 +1655,9 @@ def unregister():
del bpy.types.Scene.blendermcp_port
del bpy.types.Scene.blendermcp_server_running
del bpy.types.Scene.blendermcp_use_polyhaven
del bpy.types.Scene.blendermcp_use_hyper3d
del bpy.types.Scene.blendermcp_hyper3d_mode
del bpy.types.Scene.blendermcp_hyper3d_api_key
print("BlenderMCP addon unregistered")

View File

@ -7,6 +7,10 @@ import logging
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List
import os
from pathlib import Path
import base64
from urllib.parse import urlparse
# Configure logging
logging.basicConfig(level=logging.INFO,
@ -667,27 +671,247 @@ def get_polyhaven_status(ctx: Context) -> str:
logger.error(f"Error checking PolyHaven status: {str(e)}")
return f"Error checking PolyHaven status: {str(e)}"
@mcp.tool()
def get_hyper3d_status(ctx: Context) -> str:
"""
Check if Hyper3D Rodin integration is enabled in Blender.
Returns a message indicating whether Hyper3D Rodin features are available.
"""
try:
blender = get_blender_connection()
result = blender.send_command("get_hyper3d_status")
enabled = result.get("enabled", False)
message = result.get("message", "")
if enabled:
message += ""
return message
except Exception as e:
logger.error(f"Error checking Hyper3D status: {str(e)}")
return f"Error checking Hyper3D status: {str(e)}"
@mcp.tool()
def generate_hyper3d_model_via_text(
ctx: Context,
text_prompt: str,
bbox_condition: list[float]=None
) -> str:
"""
Generate 3D asset using Hyper3D by giving description of the desired asset, and import the asset into Blender.
The 3D asset has built-in materials.
The generated model has a normalized size, so re-scaling after generation can be useful.
Parameters:
- text_prompt: A short description of the desired model in **English**.
- bbox_condition: Optional. If given, it has to be a list of floats of length 3. Controls the ratio between [Length, Width, Height] of the model. The final size of the model is normalized.
Returns a message indicating success or failure.
"""
try:
blender = get_blender_connection()
result = blender.send_command("create_rodin_job", {
"text_prompt": text_prompt,
"images": None,
"bbox_condition": bbox_condition,
})
succeed = result.get("submit_time", False)
if succeed:
return json.dumps({
"task_uuid": result["uuid"],
"subscription_key": result["jobs"]["subscription_key"],
})
else:
return json.dumps(result)
except Exception as e:
logger.error(f"Error generating Hyper3D task: {str(e)}")
return f"Error generating Hyper3D task: {str(e)}"
return f"Placeholder, under development, not implemented yet."
@mcp.tool()
def generate_hyper3d_model_via_images(
ctx: Context,
input_image_paths: list[str]=None,
input_image_urls: list[str]=None,
bbox_condition: list[float]=None
) -> str:
"""
Generate 3D asset using Hyper3D by giving images of the wanted asset, and import the generated asset into Blender.
The 3D asset has built-in materials.
The generated model has a normalized size, so re-scaling after generation can be useful.
Parameters:
- input_image_paths: The **absolute** paths of input images. Even if only one image is provided, wrap it into a list. Required if Hyper3D Rodin in MAIN_SITE mode.
- input_image_urls: The URLs of input images. Even if only one image is provided, wrap it into a list. Required if Hyper3D Rodin in FAL_AI mode.
- bbox_condition: Optional. If given, it has to be a list of ints of length 3. Controls the ratio between [Length, Width, Height] of the model. The final size of the model is normalized.
Only one of {input_image_paths, input_image_urls} should be given at a time, depending on the Hyper3D Rodin's current mode.
Returns a message indicating success or failure.
"""
if input_image_paths is not None and input_image_urls is not None:
return f"Error: Conflict parameters given!"
if input_image_paths is None and input_image_urls is None:
return f"Error: No image given!"
if input_image_paths is not None:
if not all(os.path.exists(i) for i in input_image_paths):
return "Error: not all image paths are valid!"
images = []
for path in input_image_paths:
with open(path, "rb") as f:
images.append(
(Path(path).suffix, base64.b64encode(f.read()).decode("ascii"))
)
elif input_image_urls is not None:
if not all(urlparse(i) for i in input_image_paths):
return "Error: not all image URLs are valid!"
images = input_image_urls.copy()
try:
blender = get_blender_connection()
result = blender.send_command("create_rodin_job", {
"text_prompt": None,
"images": images,
"bbox_condition": bbox_condition,
})
succeed = result.get("submit_time", False)
if succeed:
return json.dumps({
"task_uuid": result["uuid"],
"subscription_key": result["jobs"]["subscription_key"],
})
else:
return json.dumps(result)
except Exception as e:
logger.error(f"Error generating Hyper3D task: {str(e)}")
return f"Error generating Hyper3D task: {str(e)}"
@mcp.tool()
def poll_rodin_job_status(
ctx: Context,
subscription_key: str=None,
request_id: str=None,
):
"""
Check if the Hyper3D Rodin generation task is completed.
For Hyper3D Rodin mode MAIN_SITE:
Parameters:
- subscription_key: The subscription_key given in the generate model step.
Returns a list of status. The task is done if all status are "Done".
If "Failed" showed up, the generating process failed.
This is a polling API, so only proceed if the status are finally determined ("Done" or "Canceled").
For Hyper3D Rodin mode FAL_AI:
Parameters:
- request_id: The request_id given in the generate model step.
Returns the generation task status. The task is done if status is "COMPLETED".
The task is in progress if status is "IN_PROGRESS".
If status other than "COMPLETED", "IN_PROGRESS", "IN_QUEUE" showed up, the generating process might be failed.
This is a polling API, so only proceed if the status are finally determined ("COMPLETED" or some failed state).
"""
try:
blender = get_blender_connection()
kwargs = {}
if subscription_key:
kwargs = {
"subscription_key": subscription_key,
}
elif request_id:
kwargs = {
"request_id": request_id,
}
result = blender.send_command("poll_rodin_job_status", kwargs)
return result
except Exception as e:
logger.error(f"Error generating Hyper3D task: {str(e)}")
return f"Error generating Hyper3D task: {str(e)}"
@mcp.tool()
def import_generated_asset(
ctx: Context,
name: str,
task_uuid: str=None,
request_id: str=None,
):
"""
Import the asset generated by Hyper3D Rodin after the generation task is completed.
Parameters:
- name: The name of the object in scene
- task_uuid: For Hyper3D Rodin mode MAIN_SITE: The task_uuid given in the generate model step.
- request_id: For Hyper3D Rodin mode FAL_AI: The request_id given in the generate model step.
Only give one of {task_uuid, request_id} based on the Hyper3D Rodin Mode!
Return if the asset has been imported successfully.
"""
try:
blender = get_blender_connection()
kwargs = {
"name": name
}
if task_uuid:
kwargs["task_uuid"] = task_uuid
elif request_id:
kwargs["request_id"] = request_id
result = blender.send_command("import_generated_asset", kwargs)
return result
except Exception as e:
logger.error(f"Error generating Hyper3D task: {str(e)}")
return f"Error generating Hyper3D task: {str(e)}"
@mcp.prompt()
def asset_creation_strategy() -> str:
"""Defines the preferred strategy for creating assets in Blender"""
return """When creating 3D content in Blender, always start by checking if PolyHaven is available:
return """When creating 3D content in Blender, always start by checking if integrations are available:
0. Before anything, always check the scene from get_scene_info()
1. First use get_polyhaven_status() to verify if PolyHaven integration is enabled.
2. If PolyHaven is enabled:
1. First use the following tools to verify if the following integrations are enabled:
1. PolyHaven
Use get_polyhaven_status() to verify its status
If PolyHaven is enabled:
- For objects/models: Use download_polyhaven_asset() with asset_type="models"
- For materials/textures: Use download_polyhaven_asset() with asset_type="textures"
- For environment lighting: Use download_polyhaven_asset() with asset_type="hdris"
2. Hyper3D(Rodin)
Hyper3D Rodin is good at generating 3D models for single item.
So don't try to:
1. Generate the whole scene with one shot
2. Generate ground using Rodin
3. Generate parts of the items separately and put them together afterwards
3. If PolyHaven is disabled or when falling back to basic tools:
Use get_hyper3d_status() to verify its status
If Hyper3D is enabled:
- For objects/models, do the following steps:
1. Create the model generation task
- Use generate_hyper3d_model_via_images() if image(s) is/are given
- Use generate_hyper3d_model_via_text() if generating 3D asset using text prompt
2. Poll the status
- Use poll_rodin_job_status() to check if the generation task has completed or failed
3. Import the asset
- Use import_generated_asset() to import the generated GLB model the asset
4. After importing the asset, ALWAYS check the world_bounding_box of the imported mesh, and adjust the mesh's location and size
Adjust the imported mesh's location, scale, rotation, so that the mesh is on the right spot.
You can reuse assets previous generated by repeating step 3 and 4 using the previous task_uuid without creating another generation task.
2. If all integrations are disabled or when falling back to basic tools:
- create_object() for basic primitives (CUBE, SPHERE, CYLINDER, etc.)
- set_material() for basic colors and materials
3. When including an object into scene, ALWAYS make sure that the name of the object is meanful.
4. Always check the world_bounding_box for each item so that:
- Ensure that all objects that should not be clipping are not clipping.
- Items have right spatial relationship.
5. After giving the tool location/scale/rotation information (via create_object() and modify_object()),
double check the related object's location, scale, rotation, and world_bounding_box using get_object_info(),
so that the object is in the desired location.
Only fall back to basic creation tools when:
- PolyHaven is disabled
- PolyHaven and Hyper3D are disabled
- A simple primitive is explicitly requested
- No suitable PolyHaven asset exists
- Hyper3D Rodin failed to generate the desired asset
- The task specifically requires a basic material/color
"""