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("""
|