348 lines
13 KiB
Python
348 lines
13 KiB
Python
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
|
|
# except for the third-party components listed below.
|
|
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
|
|
# in the repsective licenses of these third-party components.
|
|
# Users must comply with all terms and conditions of original licenses of these third-party
|
|
# components and must ensure that the usage of the third party components adheres to
|
|
# all relevant laws and regulations.
|
|
|
|
# For avoidance of doubts, Hunyuan 3D means the large language models and
|
|
# their software and algorithms, including trained model weights, parameters (including
|
|
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
|
|
# fine-tuning enabling code and other elements of the foregoing made publicly available
|
|
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
|
|
|
|
bl_info = {
|
|
"name": "Hunyuan3D-2 Generator",
|
|
"author": "Tencent Hunyuan3D",
|
|
"version": (1, 0),
|
|
"blender": (3, 0, 0),
|
|
"location": "View3D > Sidebar > Hunyuan3D-2 3D Generator",
|
|
"description": "Generate/Texturing 3D models from text descriptions or images",
|
|
"category": "3D View",
|
|
}
|
|
import base64
|
|
import os
|
|
import tempfile
|
|
import threading
|
|
|
|
import bpy
|
|
import requests
|
|
from bpy.props import StringProperty, BoolProperty, IntProperty, FloatProperty
|
|
|
|
|
|
class Hunyuan3DProperties(bpy.types.PropertyGroup):
|
|
prompt: StringProperty(
|
|
name="Text Prompt",
|
|
description="Describe what you want to generate",
|
|
default=""
|
|
)
|
|
api_url: StringProperty(
|
|
name="API URL",
|
|
description="URL of the Text-to-3D API service",
|
|
default="http://localhost:8080"
|
|
)
|
|
is_processing: BoolProperty(
|
|
name="Processing",
|
|
default=False
|
|
)
|
|
job_id: StringProperty(
|
|
name="Job ID",
|
|
default=""
|
|
)
|
|
status_message: StringProperty(
|
|
name="Status Message",
|
|
default=""
|
|
)
|
|
# 添加图片路径属性
|
|
image_path: StringProperty(
|
|
name="Image",
|
|
description="Select an image to upload",
|
|
subtype='FILE_PATH'
|
|
)
|
|
# 修改后的 octree_resolution 属性
|
|
octree_resolution: IntProperty(
|
|
name="Octree Resolution",
|
|
description="Octree resolution for the 3D generation",
|
|
default=256,
|
|
min=128,
|
|
max=512,
|
|
)
|
|
num_inference_steps: IntProperty(
|
|
name="Number of Inference Steps",
|
|
description="Number of inference steps for the 3D generation",
|
|
default=20,
|
|
min=20,
|
|
max=50
|
|
)
|
|
guidance_scale: FloatProperty(
|
|
name="Guidance Scale",
|
|
description="Guidance scale for the 3D generation",
|
|
default=5.5,
|
|
min=1.0,
|
|
max=10.0
|
|
)
|
|
# 添加 texture 属性
|
|
texture: BoolProperty(
|
|
name="Generate Texture",
|
|
description="Whether to generate texture for the 3D model",
|
|
default=False
|
|
)
|
|
|
|
|
|
class Hunyuan3DOperator(bpy.types.Operator):
|
|
bl_idname = "object.generate_3d"
|
|
bl_label = "Generate 3D Model"
|
|
bl_description = "Generate a 3D model from text description, an image or a selected mesh"
|
|
|
|
job_id = ''
|
|
prompt = ""
|
|
api_url = ""
|
|
image_path = ""
|
|
octree_resolution = 256
|
|
num_inference_steps = 20
|
|
guidance_scale = 5.5
|
|
texture = False # 新增属性
|
|
selected_mesh_base64 = ""
|
|
selected_mesh = None # 新增属性,用于存储选中的 mesh
|
|
|
|
thread = None
|
|
task_finished = False
|
|
|
|
def modal(self, context, event):
|
|
if event.type in {'RIGHTMOUSE', 'ESC'}:
|
|
return {'CANCELLED'}
|
|
|
|
if self.task_finished:
|
|
print("Threaded task completed")
|
|
self.task_finished = False
|
|
props = context.scene.gen_3d_props
|
|
props.is_processing = False
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
def invoke(self, context, event):
|
|
# 启动线程
|
|
props = context.scene.gen_3d_props
|
|
self.prompt = props.prompt
|
|
self.api_url = props.api_url
|
|
self.image_path = props.image_path
|
|
self.octree_resolution = props.octree_resolution
|
|
self.num_inference_steps = props.num_inference_steps
|
|
self.guidance_scale = props.guidance_scale
|
|
self.texture = props.texture # 获取 texture 属性的值
|
|
|
|
if self.prompt == "" and self.image_path == "":
|
|
self.report({'WARNING'}, "Please enter some text or select an image first.")
|
|
return {'FINISHED'}
|
|
|
|
# 保存选中的 mesh 对象引用
|
|
for obj in context.selected_objects:
|
|
if obj.type == 'MESH':
|
|
self.selected_mesh = obj
|
|
break
|
|
|
|
if self.selected_mesh:
|
|
temp_glb_file = tempfile.NamedTemporaryFile(delete=False, suffix=".glb")
|
|
temp_glb_file.close()
|
|
bpy.ops.export_scene.gltf(filepath=temp_glb_file.name, use_selection=True)
|
|
with open(temp_glb_file.name, "rb") as file:
|
|
mesh_data = file.read()
|
|
mesh_b64_str = base64.b64encode(mesh_data).decode()
|
|
os.unlink(temp_glb_file.name)
|
|
self.selected_mesh_base64 = mesh_b64_str
|
|
|
|
props.is_processing = True
|
|
|
|
# 将相对路径转换为相对于 Blender 文件所在目录的绝对路径
|
|
blend_file_dir = os.path.dirname(bpy.data.filepath)
|
|
self.report({'INFO'}, f"blend_file_dir {blend_file_dir}")
|
|
self.report({'INFO'}, f"image_path {self.image_path}")
|
|
if self.image_path.startswith('//'):
|
|
self.image_path = self.image_path[2:]
|
|
self.image_path = os.path.join(blend_file_dir, self.image_path)
|
|
|
|
if self.selected_mesh and self.texture:
|
|
props.status_message = "Texturing Selected Mesh...\n" \
|
|
"This may take several minutes depending \n on your GPU power."
|
|
else:
|
|
mesh_type = 'Textured Mesh' if self.texture else 'White Mesh'
|
|
prompt_type = 'Text Prompt' if self.prompt else 'Image'
|
|
props.status_message = f"Generating {mesh_type} with {prompt_type}...\n" \
|
|
"This may take several minutes depending \n on your GPU power."
|
|
|
|
self.thread = threading.Thread(target=self.generate_model)
|
|
self.thread.start()
|
|
|
|
wm = context.window_manager
|
|
wm.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def generate_model(self):
|
|
self.report({'INFO'}, f"Generation Start")
|
|
base_url = self.api_url.rstrip('/')
|
|
|
|
try:
|
|
if self.selected_mesh_base64 and self.texture:
|
|
# Texturing the selected mesh
|
|
if self.image_path and os.path.exists(self.image_path):
|
|
self.report({'INFO'}, f"Post Texturing with Image")
|
|
# 打开图片文件并以二进制模式读取
|
|
with open(self.image_path, "rb") as file:
|
|
# 读取文件内容
|
|
image_data = file.read()
|
|
# 对图片数据进行 Base64 编码
|
|
img_b64_str = base64.b64encode(image_data).decode()
|
|
response = requests.post(
|
|
f"{base_url}/generate",
|
|
json={
|
|
"mesh": self.selected_mesh_base64,
|
|
"image": img_b64_str,
|
|
"octree_resolution": self.octree_resolution,
|
|
"num_inference_steps": self.num_inference_steps,
|
|
"guidance_scale": self.guidance_scale,
|
|
"texture": self.texture # 传递 texture 参数
|
|
},
|
|
)
|
|
else:
|
|
self.report({'INFO'}, f"Post Texturing with Text")
|
|
response = requests.post(
|
|
f"{base_url}/generate",
|
|
json={
|
|
"mesh": self.selected_mesh_base64,
|
|
"text": self.prompt,
|
|
"octree_resolution": self.octree_resolution,
|
|
"num_inference_steps": self.num_inference_steps,
|
|
"guidance_scale": self.guidance_scale,
|
|
"texture": self.texture # 传递 texture 参数
|
|
},
|
|
)
|
|
else:
|
|
if self.image_path:
|
|
if not os.path.exists(self.image_path):
|
|
self.report({'ERROR'}, f"Image path does not exist {self.image_path}")
|
|
raise Exception(f'Image path does not exist {self.image_path}')
|
|
self.report({'INFO'}, f"Post Start Image to 3D")
|
|
# 打开图片文件并以二进制模式读取
|
|
with open(self.image_path, "rb") as file:
|
|
# 读取文件内容
|
|
image_data = file.read()
|
|
# 对图片数据进行 Base64 编码
|
|
img_b64_str = base64.b64encode(image_data).decode()
|
|
response = requests.post(
|
|
f"{base_url}/generate",
|
|
json={
|
|
"image": img_b64_str,
|
|
"octree_resolution": self.octree_resolution,
|
|
"num_inference_steps": self.num_inference_steps,
|
|
"guidance_scale": self.guidance_scale,
|
|
"texture": self.texture # 传递 texture 参数
|
|
},
|
|
)
|
|
else:
|
|
self.report({'INFO'}, f"Post Start Text to 3D")
|
|
response = requests.post(
|
|
f"{base_url}/generate",
|
|
json={
|
|
"text": self.prompt,
|
|
"octree_resolution": self.octree_resolution,
|
|
"num_inference_steps": self.num_inference_steps,
|
|
"guidance_scale": self.guidance_scale,
|
|
"texture": self.texture # 传递 texture 参数
|
|
},
|
|
)
|
|
self.report({'INFO'}, f"Post Done")
|
|
|
|
if response.status_code != 200:
|
|
self.report({'ERROR'}, f"Generation failed: {response.text}")
|
|
return
|
|
|
|
# Decode base64 and save to temporary file
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".glb")
|
|
temp_file.write(response.content)
|
|
temp_file.close()
|
|
|
|
# Import the GLB file in the main thread
|
|
def import_handler():
|
|
bpy.ops.import_scene.gltf(filepath=temp_file.name)
|
|
os.unlink(temp_file.name)
|
|
|
|
# 获取新导入的 mesh
|
|
new_obj = bpy.context.selected_objects[0] if bpy.context.selected_objects else None
|
|
if new_obj and self.selected_mesh and self.texture:
|
|
# 应用选中 mesh 的位置、旋转和缩放
|
|
new_obj.location = self.selected_mesh.location
|
|
new_obj.rotation_euler = self.selected_mesh.rotation_euler
|
|
new_obj.scale = self.selected_mesh.scale
|
|
|
|
# 隐藏原来的 mesh
|
|
self.selected_mesh.hide_set(True)
|
|
self.selected_mesh.hide_render = True
|
|
|
|
return None
|
|
|
|
bpy.app.timers.register(import_handler)
|
|
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Error: {str(e)}")
|
|
|
|
finally:
|
|
self.task_finished = True
|
|
self.selected_mesh_base64 = ""
|
|
|
|
|
|
class Hunyuan3DPanel(bpy.types.Panel):
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'Hunyuan3D-2'
|
|
bl_label = 'Hunyuan3D-2 3D Generator'
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
props = context.scene.gen_3d_props
|
|
|
|
layout.prop(props, "api_url")
|
|
layout.prop(props, "prompt")
|
|
# 添加图片选择器
|
|
layout.prop(props, "image_path")
|
|
# 添加新属性的 UI 元素
|
|
layout.prop(props, "octree_resolution")
|
|
layout.prop(props, "num_inference_steps")
|
|
layout.prop(props, "guidance_scale")
|
|
# 添加 texture 属性的 UI 元素
|
|
layout.prop(props, "texture")
|
|
|
|
row = layout.row()
|
|
row.enabled = not props.is_processing
|
|
row.operator("object.generate_3d")
|
|
|
|
if props.is_processing:
|
|
if props.status_message:
|
|
for line in props.status_message.split("\n"):
|
|
layout.label(text=line)
|
|
else:
|
|
layout.label("Processing...")
|
|
|
|
|
|
classes = (
|
|
Hunyuan3DProperties,
|
|
Hunyuan3DOperator,
|
|
Hunyuan3DPanel,
|
|
)
|
|
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.Scene.gen_3d_props = bpy.props.PointerProperty(type=Hunyuan3DProperties)
|
|
|
|
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
del bpy.types.Scene.gen_3d_props
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|