Update app.py
Browse files
app.py
CHANGED
|
@@ -244,6 +244,10 @@ light.data.energy = 2.0
|
|
| 244 |
bpy.context.scene.render.engine = 'CYCLES'
|
| 245 |
bpy.context.scene.cycles.device = 'CPU'
|
| 246 |
bpy.context.scene.render.film_transparent = True
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
"""
|
| 248 |
|
| 249 |
# Sidebar: Blender script examples
|
|
@@ -341,11 +345,10 @@ sun_light.data.energy = 5.0
|
|
| 341 |
bpy.context.scene.render.engine = 'CYCLES'
|
| 342 |
bpy.context.scene.cycles.device = 'CPU'
|
| 343 |
|
| 344 |
-
print('Solar system animation setup complete.')"""
|
| 345 |
-
}
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
| 349 |
|
| 350 |
# Clear the scene
|
| 351 |
bpy.ops.object.select_all(action='SELECT')
|
|
@@ -370,6 +373,169 @@ bpy.context.scene.camera = camera
|
|
| 370 |
|
| 371 |
# Setup lighting
|
| 372 |
bpy.ops.object.light_add(type='SUN', location=(5, -5, 5))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
"""
|
| 374 |
|
| 375 |
# Add example selector
|
|
@@ -607,11 +773,11 @@ print(f"Save operation completed, checking if file exists: {{os.path.exists(blen
|
|
| 607 |
logger.error(f"Alternative approach error: {str(e)}")
|
| 608 |
st.stop()
|
| 609 |
|
| 610 |
-
# 6) Export GLB with animation - enhanced error handling
|
| 611 |
glb_path = os.path.join(tmp_dir, "animation.glb")
|
| 612 |
|
| 613 |
-
with st.status("Exporting to GLB format...") as status:
|
| 614 |
-
# Create a separate Python script for GLB export
|
| 615 |
export_script = os.path.join(tmp_dir, "export_glb.py")
|
| 616 |
with open(export_script, "w") as f:
|
| 617 |
f.write(f"""
|
|
@@ -631,7 +797,14 @@ try:
|
|
| 631 |
bpy.ops.wm.open_mainfile(filepath=r'{blend_path}')
|
| 632 |
print("Blend file loaded successfully")
|
| 633 |
|
| 634 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
glb_path = r'{glb_path}'
|
| 636 |
print(f"Exporting to GLB: {{glb_path}}")
|
| 637 |
|
|
@@ -640,7 +813,12 @@ try:
|
|
| 640 |
bpy.ops.export_scene.gltf(
|
| 641 |
filepath=glb_path,
|
| 642 |
export_format='GLB',
|
| 643 |
-
export_animations=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
)
|
| 645 |
print(f"GLB export completed, file exists: {{os.path.exists(glb_path)}}")
|
| 646 |
else:
|
|
@@ -654,7 +832,12 @@ try:
|
|
| 654 |
bpy.ops.export_scene.gltf(
|
| 655 |
filepath=glb_path,
|
| 656 |
export_format='GLB',
|
| 657 |
-
export_animations=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
)
|
| 659 |
print(f"GLB export completed after enabling add-on")
|
| 660 |
else:
|
|
@@ -678,7 +861,7 @@ except Exception as e:
|
|
| 678 |
text=True,
|
| 679 |
timeout=180 # 3 minute timeout
|
| 680 |
)
|
| 681 |
-
status.update(label="GLB export completed successfully", state="complete")
|
| 682 |
st.text_area("Export output", r2.stdout + r2.stderr, height=100)
|
| 683 |
logger.info("GLB export completed successfully")
|
| 684 |
except Exception as e:
|
|
@@ -686,13 +869,13 @@ except Exception as e:
|
|
| 686 |
logger.error(f"GLB export error: {str(e)}")
|
| 687 |
st.stop()
|
| 688 |
|
| 689 |
-
# 7) Embed GLB inline
|
| 690 |
if os.path.exists(glb_path):
|
| 691 |
with open(glb_path, 'rb') as f:
|
| 692 |
data = f.read()
|
| 693 |
|
| 694 |
file_size = len(data) / (1024 * 1024) # Size in MB
|
| 695 |
-
st.success(f"Successfully created GLB file ({file_size:.1f} MB)")
|
| 696 |
logger.info(f"Created GLB file, size: {file_size:.1f} MB")
|
| 697 |
|
| 698 |
# Check if file isn't too large for embedding
|
|
@@ -702,25 +885,110 @@ except Exception as e:
|
|
| 702 |
|
| 703 |
b64 = base64.b64encode(data).decode()
|
| 704 |
|
| 705 |
-
# Display the 3D model viewer
|
| 706 |
-
st.subheader("3D Model Viewer")
|
| 707 |
html = f"""
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
|
| 721 |
# Add download button
|
| 722 |
st.download_button(
|
| 723 |
-
"⬇️ Download GLB File",
|
| 724 |
data,
|
| 725 |
file_name="animation.glb",
|
| 726 |
mime="model/gltf-binary"
|
|
@@ -797,6 +1065,37 @@ with st.expander("Script Tips"):
|
|
| 797 |
- Try the "Simple Cube (Test)" example first to verify Blender is working
|
| 798 |
""")
|
| 799 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 800 |
# Add a section at the bottom for Hugging Face Spaces setup
|
| 801 |
with st.expander("Hugging Face Spaces Setup"):
|
| 802 |
st.markdown("""
|
|
|
|
| 244 |
bpy.context.scene.render.engine = 'CYCLES'
|
| 245 |
bpy.context.scene.cycles.device = 'CPU'
|
| 246 |
bpy.context.scene.render.film_transparent = True
|
| 247 |
+
|
| 248 |
+
# Set frame range for animation
|
| 249 |
+
bpy.context.scene.frame_start = 1
|
| 250 |
+
bpy.context.scene.frame_end = 250
|
| 251 |
"""
|
| 252 |
|
| 253 |
# Sidebar: Blender script examples
|
|
|
|
| 345 |
bpy.context.scene.render.engine = 'CYCLES'
|
| 346 |
bpy.context.scene.cycles.device = 'CPU'
|
| 347 |
|
| 348 |
+
print('Solar system animation setup complete.')""",
|
|
|
|
| 349 |
|
| 350 |
+
"Simple Cube (Test)": """import bpy
|
| 351 |
+
import math
|
| 352 |
|
| 353 |
# Clear the scene
|
| 354 |
bpy.ops.object.select_all(action='SELECT')
|
|
|
|
| 373 |
|
| 374 |
# Setup lighting
|
| 375 |
bpy.ops.object.light_add(type='SUN', location=(5, -5, 5))
|
| 376 |
+
|
| 377 |
+
# Set frame range
|
| 378 |
+
bpy.context.scene.frame_start = 1
|
| 379 |
+
bpy.context.scene.frame_end = 100
|
| 380 |
+
"""
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
# Add a more animation-focused example
|
| 384 |
+
script_examples["Animated Character"] = """import bpy
|
| 385 |
+
import math
|
| 386 |
+
|
| 387 |
+
# Clear existing objects
|
| 388 |
+
bpy.ops.object.select_all(action='SELECT')
|
| 389 |
+
bpy.ops.object.delete(use_global=False)
|
| 390 |
+
|
| 391 |
+
# Create simple character with basic armature
|
| 392 |
+
def create_simple_character():
|
| 393 |
+
# Create a body (cylinder)
|
| 394 |
+
bpy.ops.mesh.primitive_cylinder_add(radius=0.5, depth=2, location=(0, 0, 1))
|
| 395 |
+
body = bpy.context.active_object
|
| 396 |
+
body.name = "Body"
|
| 397 |
+
|
| 398 |
+
# Create a head (sphere)
|
| 399 |
+
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.5, location=(0, 0, 2.5))
|
| 400 |
+
head = bpy.context.active_object
|
| 401 |
+
head.name = "Head"
|
| 402 |
+
|
| 403 |
+
# Create arms (cylinders)
|
| 404 |
+
bpy.ops.mesh.primitive_cylinder_add(radius=0.2, depth=1.5, location=(0.8, 0, 1.5))
|
| 405 |
+
arm_right = bpy.context.active_object
|
| 406 |
+
arm_right.name = "ArmRight"
|
| 407 |
+
arm_right.rotation_euler = (0, math.radians(90), 0)
|
| 408 |
+
|
| 409 |
+
bpy.ops.mesh.primitive_cylinder_add(radius=0.2, depth=1.5, location=(-0.8, 0, 1.5))
|
| 410 |
+
arm_left = bpy.context.active_object
|
| 411 |
+
arm_left.name = "ArmLeft"
|
| 412 |
+
arm_left.rotation_euler = (0, math.radians(90), 0)
|
| 413 |
+
|
| 414 |
+
# Create legs (cylinders)
|
| 415 |
+
bpy.ops.mesh.primitive_cylinder_add(radius=0.25, depth=1, location=(0.3, 0, 0))
|
| 416 |
+
leg_right = bpy.context.active_object
|
| 417 |
+
leg_right.name = "LegRight"
|
| 418 |
+
|
| 419 |
+
bpy.ops.mesh.primitive_cylinder_add(radius=0.25, depth=1, location=(-0.3, 0, 0))
|
| 420 |
+
leg_left = bpy.context.active_object
|
| 421 |
+
leg_left.name = "LegLeft"
|
| 422 |
+
|
| 423 |
+
return {
|
| 424 |
+
"body": body,
|
| 425 |
+
"head": head,
|
| 426 |
+
"arm_right": arm_right,
|
| 427 |
+
"arm_left": arm_left,
|
| 428 |
+
"leg_right": leg_right,
|
| 429 |
+
"leg_left": leg_left
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
# Create character
|
| 433 |
+
character = create_simple_character()
|
| 434 |
+
|
| 435 |
+
# Create dance animation
|
| 436 |
+
def create_dance_animation():
|
| 437 |
+
# Set frame range
|
| 438 |
+
bpy.context.scene.frame_start = 1
|
| 439 |
+
bpy.context.scene.frame_end = 60
|
| 440 |
+
|
| 441 |
+
# Arm waving
|
| 442 |
+
# Frame 1
|
| 443 |
+
bpy.context.scene.frame_set(1)
|
| 444 |
+
character["arm_right"].rotation_euler = (0, math.radians(90), 0)
|
| 445 |
+
character["arm_left"].rotation_euler = (0, math.radians(90), 0)
|
| 446 |
+
character["arm_right"].keyframe_insert(data_path="rotation_euler")
|
| 447 |
+
character["arm_left"].keyframe_insert(data_path="rotation_euler")
|
| 448 |
+
|
| 449 |
+
# Frame 15
|
| 450 |
+
bpy.context.scene.frame_set(15)
|
| 451 |
+
character["arm_right"].rotation_euler = (0, math.radians(90), math.radians(45))
|
| 452 |
+
character["arm_left"].rotation_euler = (0, math.radians(90), math.radians(-45))
|
| 453 |
+
character["arm_right"].keyframe_insert(data_path="rotation_euler")
|
| 454 |
+
character["arm_left"].keyframe_insert(data_path="rotation_euler")
|
| 455 |
+
|
| 456 |
+
# Frame 30
|
| 457 |
+
bpy.context.scene.frame_set(30)
|
| 458 |
+
character["arm_right"].rotation_euler = (0, math.radians(90), math.radians(-45))
|
| 459 |
+
character["arm_left"].rotation_euler = (0, math.radians(90), math.radians(45))
|
| 460 |
+
character["arm_right"].keyframe_insert(data_path="rotation_euler")
|
| 461 |
+
character["arm_left"].keyframe_insert(data_path="rotation_euler")
|
| 462 |
+
|
| 463 |
+
# Frame 45
|
| 464 |
+
bpy.context.scene.frame_set(45)
|
| 465 |
+
character["arm_right"].rotation_euler = (0, math.radians(90), math.radians(45))
|
| 466 |
+
character["arm_left"].rotation_euler = (0, math.radians(90), math.radians(-45))
|
| 467 |
+
character["arm_right"].keyframe_insert(data_path="rotation_euler")
|
| 468 |
+
character["arm_left"].keyframe_insert(data_path="rotation_euler")
|
| 469 |
+
|
| 470 |
+
# Frame 60
|
| 471 |
+
bpy.context.scene.frame_set(60)
|
| 472 |
+
character["arm_right"].rotation_euler = (0, math.radians(90), 0)
|
| 473 |
+
character["arm_left"].rotation_euler = (0, math.radians(90), 0)
|
| 474 |
+
character["arm_right"].keyframe_insert(data_path="rotation_euler")
|
| 475 |
+
character["arm_left"].keyframe_insert(data_path="rotation_euler")
|
| 476 |
+
|
| 477 |
+
# Body bounce
|
| 478 |
+
# Frame 1
|
| 479 |
+
bpy.context.scene.frame_set(1)
|
| 480 |
+
character["body"].location = (0, 0, 1)
|
| 481 |
+
character["body"].keyframe_insert(data_path="location")
|
| 482 |
+
character["head"].location = (0, 0, 2.5)
|
| 483 |
+
character["head"].keyframe_insert(data_path="location")
|
| 484 |
+
|
| 485 |
+
# Frame 15
|
| 486 |
+
bpy.context.scene.frame_set(15)
|
| 487 |
+
character["body"].location = (0, 0, 1.2)
|
| 488 |
+
character["body"].keyframe_insert(data_path="location")
|
| 489 |
+
character["head"].location = (0, 0, 2.7)
|
| 490 |
+
character["head"].keyframe_insert(data_path="location")
|
| 491 |
+
|
| 492 |
+
# Frame 30
|
| 493 |
+
bpy.context.scene.frame_set(30)
|
| 494 |
+
character["body"].location = (0, 0, 1)
|
| 495 |
+
character["body"].keyframe_insert(data_path="location")
|
| 496 |
+
character["head"].location = (0, 0, 2.5)
|
| 497 |
+
character["head"].keyframe_insert(data_path="location")
|
| 498 |
+
|
| 499 |
+
# Frame 45
|
| 500 |
+
bpy.context.scene.frame_set(45)
|
| 501 |
+
character["body"].location = (0, 0, 1.2)
|
| 502 |
+
character["body"].keyframe_insert(data_path="location")
|
| 503 |
+
character["head"].location = (0, 0, 2.7)
|
| 504 |
+
character["head"].keyframe_insert(data_path="location")
|
| 505 |
+
|
| 506 |
+
# Frame 60
|
| 507 |
+
bpy.context.scene.frame_set(60)
|
| 508 |
+
character["body"].location = (0, 0, 1)
|
| 509 |
+
character["body"].keyframe_insert(data_path="location")
|
| 510 |
+
character["head"].location = (0, 0, 2.5)
|
| 511 |
+
character["head"].keyframe_insert(data_path="location")
|
| 512 |
+
|
| 513 |
+
# Set interpolation
|
| 514 |
+
for obj in character.values():
|
| 515 |
+
if obj.animation_data and obj.animation_data.action:
|
| 516 |
+
for fc in obj.animation_data.action.fcurves:
|
| 517 |
+
for kp in fc.keyframe_points:
|
| 518 |
+
kp.interpolation = 'BEZIER'
|
| 519 |
+
|
| 520 |
+
# Create the animation
|
| 521 |
+
create_dance_animation()
|
| 522 |
+
|
| 523 |
+
# Set up camera and lighting
|
| 524 |
+
bpy.ops.object.camera_add(location=(0, -5, 2))
|
| 525 |
+
camera = bpy.context.active_object
|
| 526 |
+
camera.rotation_euler = (math.radians(80), 0, 0)
|
| 527 |
+
bpy.context.scene.camera = camera
|
| 528 |
+
|
| 529 |
+
# Add light
|
| 530 |
+
bpy.ops.object.light_add(type='SUN', location=(2, -3, 5))
|
| 531 |
+
sun = bpy.context.active_object
|
| 532 |
+
sun.data.energy = 3.0
|
| 533 |
+
|
| 534 |
+
# Set render settings
|
| 535 |
+
bpy.context.scene.render.engine = 'CYCLES'
|
| 536 |
+
bpy.context.scene.cycles.device = 'CPU'
|
| 537 |
+
|
| 538 |
+
print('Character animation setup complete')
|
| 539 |
"""
|
| 540 |
|
| 541 |
# Add example selector
|
|
|
|
| 773 |
logger.error(f"Alternative approach error: {str(e)}")
|
| 774 |
st.stop()
|
| 775 |
|
| 776 |
+
# 6) Export GLB with animation - enhanced error handling and animation support
|
| 777 |
glb_path = os.path.join(tmp_dir, "animation.glb")
|
| 778 |
|
| 779 |
+
with st.status("Exporting to GLB format with animations...") as status:
|
| 780 |
+
# Create a separate Python script for enhanced GLB export with better animation support
|
| 781 |
export_script = os.path.join(tmp_dir, "export_glb.py")
|
| 782 |
with open(export_script, "w") as f:
|
| 783 |
f.write(f"""
|
|
|
|
| 797 |
bpy.ops.wm.open_mainfile(filepath=r'{blend_path}')
|
| 798 |
print("Blend file loaded successfully")
|
| 799 |
|
| 800 |
+
# Get animation info
|
| 801 |
+
scene = bpy.context.scene
|
| 802 |
+
frame_start = scene.frame_start
|
| 803 |
+
frame_end = scene.frame_end
|
| 804 |
+
fps = scene.render.fps
|
| 805 |
+
print(f"Animation frames: {{frame_start}}-{{frame_end}} at {{fps}} fps")
|
| 806 |
+
|
| 807 |
+
# Export to GLB with enhanced animation options
|
| 808 |
glb_path = r'{glb_path}'
|
| 809 |
print(f"Exporting to GLB: {{glb_path}}")
|
| 810 |
|
|
|
|
| 813 |
bpy.ops.export_scene.gltf(
|
| 814 |
filepath=glb_path,
|
| 815 |
export_format='GLB',
|
| 816 |
+
export_animations=True,
|
| 817 |
+
export_frame_range=True,
|
| 818 |
+
export_frame_step=1,
|
| 819 |
+
export_anim_single_armature=False, # Export all animations
|
| 820 |
+
export_current_frame=False,
|
| 821 |
+
export_apply=False # Keep animations intact
|
| 822 |
)
|
| 823 |
print(f"GLB export completed, file exists: {{os.path.exists(glb_path)}}")
|
| 824 |
else:
|
|
|
|
| 832 |
bpy.ops.export_scene.gltf(
|
| 833 |
filepath=glb_path,
|
| 834 |
export_format='GLB',
|
| 835 |
+
export_animations=True,
|
| 836 |
+
export_frame_range=True,
|
| 837 |
+
export_frame_step=1,
|
| 838 |
+
export_anim_single_armature=False,
|
| 839 |
+
export_current_frame=False,
|
| 840 |
+
export_apply=False
|
| 841 |
)
|
| 842 |
print(f"GLB export completed after enabling add-on")
|
| 843 |
else:
|
|
|
|
| 861 |
text=True,
|
| 862 |
timeout=180 # 3 minute timeout
|
| 863 |
)
|
| 864 |
+
status.update(label="GLB export with animations completed successfully", state="complete")
|
| 865 |
st.text_area("Export output", r2.stdout + r2.stderr, height=100)
|
| 866 |
logger.info("GLB export completed successfully")
|
| 867 |
except Exception as e:
|
|
|
|
| 869 |
logger.error(f"GLB export error: {str(e)}")
|
| 870 |
st.stop()
|
| 871 |
|
| 872 |
+
# 7) Embed GLB inline with enhanced animation viewer
|
| 873 |
if os.path.exists(glb_path):
|
| 874 |
with open(glb_path, 'rb') as f:
|
| 875 |
data = f.read()
|
| 876 |
|
| 877 |
file_size = len(data) / (1024 * 1024) # Size in MB
|
| 878 |
+
st.success(f"Successfully created GLB file with animations ({file_size:.1f} MB)")
|
| 879 |
logger.info(f"Created GLB file, size: {file_size:.1f} MB")
|
| 880 |
|
| 881 |
# Check if file isn't too large for embedding
|
|
|
|
| 885 |
|
| 886 |
b64 = base64.b64encode(data).decode()
|
| 887 |
|
| 888 |
+
# Display the 3D model viewer with animation controls
|
| 889 |
+
st.subheader("3D Model Viewer with Animation Controls")
|
| 890 |
html = f"""
|
| 891 |
+
<script type="module" src="https://unpkg.com/@google/model-viewer@latest/dist/model-viewer.min.js"></script>
|
| 892 |
+
<style>
|
| 893 |
+
.animation-controls {{
|
| 894 |
+
display: flex;
|
| 895 |
+
justify-content: center;
|
| 896 |
+
margin-top: 10px;
|
| 897 |
+
gap: 8px;
|
| 898 |
+
}}
|
| 899 |
+
.animation-btn {{
|
| 900 |
+
background-color: #4CAF50;
|
| 901 |
+
border: none;
|
| 902 |
+
color: white;
|
| 903 |
+
padding: 8px 16px;
|
| 904 |
+
text-align: center;
|
| 905 |
+
text-decoration: none;
|
| 906 |
+
display: inline-block;
|
| 907 |
+
font-size: 14px;
|
| 908 |
+
margin: 4px 2px;
|
| 909 |
+
cursor: pointer;
|
| 910 |
+
border-radius: 4px;
|
| 911 |
+
}}
|
| 912 |
+
#animation-name {{
|
| 913 |
+
font-weight: bold;
|
| 914 |
+
margin-right: 10px;
|
| 915 |
+
}}
|
| 916 |
+
</style>
|
| 917 |
+
|
| 918 |
+
<model-viewer id="model-viewer"
|
| 919 |
+
src="data:model/gltf-binary;base64,{b64}"
|
| 920 |
+
alt="3D Model with Animation"
|
| 921 |
+
camera-controls
|
| 922 |
+
auto-rotate
|
| 923 |
+
ar
|
| 924 |
+
shadow-intensity="1"
|
| 925 |
+
animation-name=""
|
| 926 |
+
autoplay
|
| 927 |
+
style="width:100%; height:500px; background-color: #f0f0f0;">
|
| 928 |
+
<div class="progress-bar hide" slot="progress-bar">
|
| 929 |
+
<div class="update-bar"></div>
|
| 930 |
+
</div>
|
| 931 |
+
</model-viewer>
|
| 932 |
+
|
| 933 |
+
<div class="animation-controls">
|
| 934 |
+
<span id="animation-name">No animation playing</span>
|
| 935 |
+
<button id="play-btn" class="animation-btn">Play</button>
|
| 936 |
+
<button id="pause-btn" class="animation-btn">Pause</button>
|
| 937 |
+
<button id="stop-btn" class="animation-btn">Stop</button>
|
| 938 |
+
</div>
|
| 939 |
+
|
| 940 |
+
<script>
|
| 941 |
+
const modelViewer = document.getElementById('model-viewer');
|
| 942 |
+
const playBtn = document.getElementById('play-btn');
|
| 943 |
+
const pauseBtn = document.getElementById('pause-btn');
|
| 944 |
+
const stopBtn = document.getElementById('stop-btn');
|
| 945 |
+
const animNameDisplay = document.getElementById('animation-name');
|
| 946 |
+
|
| 947 |
+
// Wait for the model to load
|
| 948 |
+
modelViewer.addEventListener('load', () => {{
|
| 949 |
+
// Get available animations
|
| 950 |
+
const animationNames = modelViewer.availableAnimations;
|
| 951 |
+
console.log('Available animations:', animationNames);
|
| 952 |
+
|
| 953 |
+
if (animationNames && animationNames.length > 0) {{
|
| 954 |
+
// Set first animation as default
|
| 955 |
+
modelViewer.animationName = animationNames[0];
|
| 956 |
+
animNameDisplay.textContent = 'Playing: ' + animationNames[0];
|
| 957 |
+
modelViewer.play();
|
| 958 |
+
}} else {{
|
| 959 |
+
animNameDisplay.textContent = 'No animations available';
|
| 960 |
+
}}
|
| 961 |
+
}});
|
| 962 |
+
|
| 963 |
+
// Animation controls
|
| 964 |
+
playBtn.addEventListener('click', () => {{
|
| 965 |
+
modelViewer.play();
|
| 966 |
+
if (modelViewer.animationName) {{
|
| 967 |
+
animNameDisplay.textContent = 'Playing: ' + modelViewer.animationName;
|
| 968 |
+
}}
|
| 969 |
+
}});
|
| 970 |
+
|
| 971 |
+
pauseBtn.addEventListener('click', () => {{
|
| 972 |
+
modelViewer.pause();
|
| 973 |
+
if (modelViewer.animationName) {{
|
| 974 |
+
animNameDisplay.textContent = 'Paused: ' + modelViewer.animationName;
|
| 975 |
+
}}
|
| 976 |
+
}});
|
| 977 |
+
|
| 978 |
+
stopBtn.addEventListener('click', () => {{
|
| 979 |
+
modelViewer.pause();
|
| 980 |
+
modelViewer.currentTime = 0;
|
| 981 |
+
if (modelViewer.animationName) {{
|
| 982 |
+
animNameDisplay.textContent = 'Stopped: ' + modelViewer.animationName;
|
| 983 |
+
}}
|
| 984 |
+
}});
|
| 985 |
+
</script>
|
| 986 |
+
"""
|
| 987 |
+
st.components.v1.html(html, height=600)
|
| 988 |
|
| 989 |
# Add download button
|
| 990 |
st.download_button(
|
| 991 |
+
"⬇️ Download GLB File with Animation",
|
| 992 |
data,
|
| 993 |
file_name="animation.glb",
|
| 994 |
mime="model/gltf-binary"
|
|
|
|
| 1065 |
- Try the "Simple Cube (Test)" example first to verify Blender is working
|
| 1066 |
""")
|
| 1067 |
|
| 1068 |
+
with st.expander("Animation Tips"):
|
| 1069 |
+
st.markdown("""
|
| 1070 |
+
### Tips for creating animations that show properly in GLB
|
| 1071 |
+
|
| 1072 |
+
1. **Set frame range properly**: Always define your animation frame range
|
| 1073 |
+
```python
|
| 1074 |
+
bpy.context.scene.frame_start = 1
|
| 1075 |
+
bpy.context.scene.frame_end = 60 # End frame of your animation
|
| 1076 |
+
```
|
| 1077 |
+
|
| 1078 |
+
2. **Use keyframes properly**: Add keyframes at specific frames
|
| 1079 |
+
```python
|
| 1080 |
+
# Set current frame
|
| 1081 |
+
bpy.context.scene.frame_set(1)
|
| 1082 |
+
|
| 1083 |
+
# Set object properties
|
| 1084 |
+
obj.location = (0, 0, 0)
|
| 1085 |
+
|
| 1086 |
+
# Insert keyframe for that property
|
| 1087 |
+
obj.keyframe_insert(data_path="location")
|
| 1088 |
+
```
|
| 1089 |
+
|
| 1090 |
+
3. **Animation export settings**: The app automatically handles GLB export with proper animation settings
|
| 1091 |
+
|
| 1092 |
+
4. **Animation playback**: Use the animation controls below the 3D viewer to play/pause/stop your animation
|
| 1093 |
+
|
| 1094 |
+
5. **Multiple animations**: If your GLB has multiple animations, they will be detected and can be selected in the viewer
|
| 1095 |
+
|
| 1096 |
+
6. **Looping animations**: By default, animations will loop in the viewer
|
| 1097 |
+
""")
|
| 1098 |
+
|
| 1099 |
# Add a section at the bottom for Hugging Face Spaces setup
|
| 1100 |
with st.expander("Hugging Face Spaces Setup"):
|
| 1101 |
st.markdown("""
|