Feat add: Added Hyper3D support.
This commit is contained in:
parent
430821326d
commit
cb2b825a30
356
addon.py
356
addon.py
@ -163,6 +163,7 @@ class BlenderMCPServer:
|
|||||||
"execute_code": self.execute_code,
|
"execute_code": self.execute_code,
|
||||||
"set_material": self.set_material,
|
"set_material": self.set_material,
|
||||||
"get_polyhaven_status": self.get_polyhaven_status,
|
"get_polyhaven_status": self.get_polyhaven_status,
|
||||||
|
"get_hyper3d_status": self.get_hyper3d_status,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add Polyhaven handlers only if enabled
|
# Add Polyhaven handlers only if enabled
|
||||||
@ -175,6 +176,15 @@ class BlenderMCPServer:
|
|||||||
}
|
}
|
||||||
handlers.update(polyhaven_handlers)
|
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)
|
handler = handlers.get(cmd_type)
|
||||||
if handler:
|
if handler:
|
||||||
try:
|
try:
|
||||||
@ -1204,6 +1214,322 @@ class BlenderMCPServer:
|
|||||||
3. Restart the connection to Claude"""
|
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
|
# 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"
|
||||||
@ -1219,6 +1545,11 @@ class BLENDERMCP_PT_Panel(bpy.types.Panel):
|
|||||||
layout.prop(scene, "blendermcp_port")
|
layout.prop(scene, "blendermcp_port")
|
||||||
layout.prop(scene, "blendermcp_use_polyhaven", text="Use assets from Poly Haven")
|
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:
|
if not scene.blendermcp_server_running:
|
||||||
layout.operator("blendermcp.start_server", text="Start MCP Server")
|
layout.operator("blendermcp.start_server", text="Start MCP Server")
|
||||||
else:
|
else:
|
||||||
@ -1283,6 +1614,28 @@ def register():
|
|||||||
default=False
|
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_PT_Panel)
|
||||||
bpy.utils.register_class(BLENDERMCP_OT_StartServer)
|
bpy.utils.register_class(BLENDERMCP_OT_StartServer)
|
||||||
bpy.utils.register_class(BLENDERMCP_OT_StopServer)
|
bpy.utils.register_class(BLENDERMCP_OT_StopServer)
|
||||||
@ -1302,6 +1655,9 @@ def unregister():
|
|||||||
del bpy.types.Scene.blendermcp_port
|
del bpy.types.Scene.blendermcp_port
|
||||||
del bpy.types.Scene.blendermcp_server_running
|
del bpy.types.Scene.blendermcp_server_running
|
||||||
del bpy.types.Scene.blendermcp_use_polyhaven
|
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")
|
print("BlenderMCP addon unregistered")
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,10 @@ import logging
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import AsyncIterator, Dict, Any, List
|
from typing import AsyncIterator, Dict, Any, List
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import base64
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO,
|
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)}")
|
logger.error(f"Error checking PolyHaven status: {str(e)}")
|
||||||
return 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()
|
@mcp.prompt()
|
||||||
def asset_creation_strategy() -> str:
|
def asset_creation_strategy() -> str:
|
||||||
"""Defines the preferred strategy for creating assets in Blender"""
|
"""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()
|
0. Before anything, always check the scene from get_scene_info()
|
||||||
1. First use get_polyhaven_status() to verify if PolyHaven integration 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
|
||||||
|
|
||||||
2. If PolyHaven is enabled:
|
Use get_hyper3d_status() to verify its status
|
||||||
- For objects/models: Use download_polyhaven_asset() with asset_type="models"
|
If Hyper3D is enabled:
|
||||||
- For materials/textures: Use download_polyhaven_asset() with asset_type="textures"
|
- For objects/models, do the following steps:
|
||||||
- For environment lighting: Use download_polyhaven_asset() with asset_type="hdris"
|
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.
|
||||||
|
|
||||||
3. If PolyHaven is disabled or when falling back to basic tools:
|
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.)
|
- create_object() for basic primitives (CUBE, SPHERE, CYLINDER, etc.)
|
||||||
- set_material() for basic colors and materials
|
- 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:
|
Only fall back to basic creation tools when:
|
||||||
- PolyHaven is disabled
|
- PolyHaven and Hyper3D are disabled
|
||||||
- A simple primitive is explicitly requested
|
- A simple primitive is explicitly requested
|
||||||
- No suitable PolyHaven asset exists
|
- No suitable PolyHaven asset exists
|
||||||
|
- Hyper3D Rodin failed to generate the desired asset
|
||||||
- The task specifically requires a basic material/color
|
- The task specifically requires a basic material/color
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user