Update app.py
Browse files
app.py
CHANGED
@@ -6,6 +6,7 @@ import requests
|
|
6 |
import base64
|
7 |
import sys
|
8 |
import shutil
|
|
|
9 |
from streamlit_ace import st_ace
|
10 |
|
11 |
st.set_page_config(page_title="Blender 3D Viewer", layout="wide")
|
@@ -34,6 +35,37 @@ def find_blender():
|
|
34 |
|
35 |
return None
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
# Check if Blender is available at startup
|
38 |
blender_path = find_blender()
|
39 |
if not blender_path:
|
@@ -50,7 +82,6 @@ DEFAULT_TEXTURE_URLS = [
|
|
50 |
# Sidebar: Blender script editor with example script
|
51 |
default_script = """
|
52 |
import bpy
|
53 |
-
import os
|
54 |
import math
|
55 |
|
56 |
# Clear the scene
|
@@ -126,17 +157,116 @@ light.data.energy = 2.0
|
|
126 |
bpy.context.scene.render.engine = 'CYCLES'
|
127 |
bpy.context.scene.cycles.device = 'CPU'
|
128 |
bpy.context.scene.render.film_transparent = True
|
129 |
-
|
130 |
-
# Save the .blend file using an absolute path
|
131 |
-
output_dir = os.environ.get('BLENDER_OUTPUT_DIR', os.getcwd())
|
132 |
-
blend_file_path = os.path.join(output_dir, "scene.blend")
|
133 |
-
bpy.ops.wm.save_as_mainfile(filepath=blend_file_path)
|
134 |
"""
|
135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
# Sidebar: Blender script editor
|
137 |
st.sidebar.header("Blender Python Script")
|
138 |
script_text = st_ace(
|
139 |
-
value=
|
140 |
placeholder="Paste your Blender Python script here...",
|
141 |
language="python",
|
142 |
theme="monokai",
|
@@ -172,10 +302,11 @@ if st.sidebar.button("Run & Export GLB"):
|
|
172 |
# Use a more robust temporary directory approach
|
173 |
tmp_dir = tempfile.mkdtemp(prefix="blender_app_")
|
174 |
try:
|
175 |
-
# 1)
|
|
|
176 |
script_path = os.path.join(tmp_dir, "user_script.py")
|
177 |
with open(script_path, "w") as f:
|
178 |
-
f.write(
|
179 |
|
180 |
# 2) Collect textures
|
181 |
texture_paths = []
|
@@ -367,14 +498,8 @@ with st.expander("About this app"):
|
|
367 |
- Generate and visualize 3D models with animations
|
368 |
- Download the results as GLB files for use in other applications
|
369 |
|
370 |
-
The
|
371 |
-
|
372 |
-
### Troubleshooting
|
373 |
-
|
374 |
-
- If you encounter errors, check the console output for details
|
375 |
-
- Make sure your script correctly saves files using absolute paths
|
376 |
-
- For complex models, allow more time for processing
|
377 |
-
- If the app doesn't work at all, the Hugging Face Space might need proper Blender installation
|
378 |
""")
|
379 |
|
380 |
with st.expander("Script Tips"):
|
@@ -390,14 +515,15 @@ with st.expander("Script Tips"):
|
|
390 |
image = bpy.data.images.load(texture_paths[0])
|
391 |
```
|
392 |
|
393 |
-
2. **
|
|
|
|
|
394 |
|
395 |
```python
|
396 |
-
# Example
|
397 |
-
|
398 |
-
|
399 |
-
|
|
|
400 |
```
|
401 |
-
|
402 |
-
3. **Animations**: Make sure to set keyframes if you want your model to animate
|
403 |
""")
|
|
|
6 |
import base64
|
7 |
import sys
|
8 |
import shutil
|
9 |
+
import re
|
10 |
from streamlit_ace import st_ace
|
11 |
|
12 |
st.set_page_config(page_title="Blender 3D Viewer", layout="wide")
|
|
|
35 |
|
36 |
return None
|
37 |
|
38 |
+
# Preprocess script to ensure it has necessary imports and path handling
|
39 |
+
def preprocess_script(script_text, tmp_dir):
|
40 |
+
# Check if 'import os' exists in the script
|
41 |
+
if 'import os' not in script_text:
|
42 |
+
script_text = 'import os\n' + script_text
|
43 |
+
|
44 |
+
# Check if 'import math' exists in the script (commonly needed)
|
45 |
+
if 'import math' not in script_text and 'math.' in script_text:
|
46 |
+
script_text = 'import math\n' + script_text
|
47 |
+
|
48 |
+
# Check if script already has file saving code
|
49 |
+
if 'bpy.ops.wm.save_as_mainfile' not in script_text:
|
50 |
+
# Add file saving code at the end
|
51 |
+
script_text += f'''
|
52 |
+
|
53 |
+
# Save the .blend file
|
54 |
+
output_dir = os.environ.get('BLENDER_OUTPUT_DIR', os.getcwd())
|
55 |
+
blend_file_path = os.path.join(output_dir, "scene.blend")
|
56 |
+
bpy.ops.wm.save_as_mainfile(filepath=blend_file_path)
|
57 |
+
print(f"Saved blend file to: {{blend_file_path}}")
|
58 |
+
'''
|
59 |
+
else:
|
60 |
+
# Make sure existing save code uses absolute paths correctly
|
61 |
+
script_text = re.sub(
|
62 |
+
r'bpy\.ops\.wm\.save_as_mainfile\s*\(\s*filepath\s*=\s*["\']([^"\']+)["\']',
|
63 |
+
'bpy.ops.wm.save_as_mainfile(filepath=os.path.join(os.environ.get("BLENDER_OUTPUT_DIR", os.getcwd()), "scene.blend")',
|
64 |
+
script_text
|
65 |
+
)
|
66 |
+
|
67 |
+
return script_text
|
68 |
+
|
69 |
# Check if Blender is available at startup
|
70 |
blender_path = find_blender()
|
71 |
if not blender_path:
|
|
|
82 |
# Sidebar: Blender script editor with example script
|
83 |
default_script = """
|
84 |
import bpy
|
|
|
85 |
import math
|
86 |
|
87 |
# Clear the scene
|
|
|
157 |
bpy.context.scene.render.engine = 'CYCLES'
|
158 |
bpy.context.scene.cycles.device = 'CPU'
|
159 |
bpy.context.scene.render.film_transparent = True
|
|
|
|
|
|
|
|
|
|
|
160 |
"""
|
161 |
|
162 |
+
# Sidebar: Blender script examples
|
163 |
+
script_examples = {
|
164 |
+
"Earth with Texture": default_script,
|
165 |
+
"Solar System": """import bpy
|
166 |
+
import math
|
167 |
+
|
168 |
+
# Clear existing objects
|
169 |
+
bpy.ops.object.select_all(action='SELECT')
|
170 |
+
bpy.ops.object.delete(use_global=False)
|
171 |
+
|
172 |
+
# Function to create a sphere (planet or sun)
|
173 |
+
def create_celestial_body(name, radius, location, color):
|
174 |
+
# Create mesh sphere
|
175 |
+
bpy.ops.mesh.primitive_uv_sphere_add(radius=radius, location=location)
|
176 |
+
obj = bpy.context.active_object
|
177 |
+
obj.name = name
|
178 |
+
|
179 |
+
# Create material
|
180 |
+
mat = bpy.data.materials.new(name + "_Material")
|
181 |
+
mat.use_nodes = True
|
182 |
+
bsdf = mat.node_tree.nodes.get('Principled BSDF')
|
183 |
+
bsdf.inputs['Base Color'].default_value = (*color, 1)
|
184 |
+
bsdf.inputs['Roughness'].default_value = 0.5
|
185 |
+
|
186 |
+
# Assign material
|
187 |
+
if obj.data.materials:
|
188 |
+
obj.data.materials[0] = mat
|
189 |
+
else:
|
190 |
+
obj.data.materials.append(mat)
|
191 |
+
return obj
|
192 |
+
|
193 |
+
# Create the Sun (yellow)
|
194 |
+
sun = create_celestial_body('Sun', radius=2, location=(0,0,0), color=(1.0, 1.0, 0.0))
|
195 |
+
|
196 |
+
# Create an empty at origin to parent planets for orbiting
|
197 |
+
def create_orbit_empty(name):
|
198 |
+
bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0,0,0))
|
199 |
+
empty = bpy.context.active_object
|
200 |
+
empty.name = name
|
201 |
+
return empty
|
202 |
+
|
203 |
+
# Planet definitions: name, radius, distance from sun, color, orbital period in frames
|
204 |
+
planets = [
|
205 |
+
('Mercury', 0.3, 4, (0.8, 0.5, 0.2), 88),
|
206 |
+
('Venus', 0.5, 6, (1.0, 0.8, 0.0), 224),
|
207 |
+
('Earth', 0.5, 8, (0.2, 0.4, 1.0), 365),
|
208 |
+
('Mars', 0.4, 10,(1.0, 0.3, 0.2), 687)
|
209 |
+
]
|
210 |
+
|
211 |
+
for name, radius, distance, color, period in planets:
|
212 |
+
# Create orbit controller
|
213 |
+
orbit_empty = create_orbit_empty(name + '_Orbit')
|
214 |
+
|
215 |
+
# Create planet as child of orbit empty
|
216 |
+
planet = create_celestial_body(name, radius, location=(distance, 0, 0), color=color)
|
217 |
+
planet.parent = orbit_empty
|
218 |
+
|
219 |
+
# Animate the orbit: rotate empty around Z
|
220 |
+
orbit_empty.rotation_euler = (0, 0, 0)
|
221 |
+
orbit_empty.keyframe_insert(data_path='rotation_euler', frame=1, index=2)
|
222 |
+
orbit_empty.rotation_euler = (0, 0, math.radians(360))
|
223 |
+
orbit_empty.keyframe_insert(data_path='rotation_euler', frame=period, index=2)
|
224 |
+
|
225 |
+
# Set linear interpolation for smooth constant motion
|
226 |
+
action = orbit_empty.animation_data.action
|
227 |
+
fcurve = action.fcurves.find('rotation_euler', index=2)
|
228 |
+
if fcurve:
|
229 |
+
for kp in fcurve.keyframe_points:
|
230 |
+
kp.interpolation = 'LINEAR'
|
231 |
+
|
232 |
+
# Set scene frames
|
233 |
+
bpy.context.scene.frame_start = 1
|
234 |
+
bpy.context.scene.frame_end = 687 # Use Mars' period as the end frame
|
235 |
+
|
236 |
+
# Add simple starfield as world background
|
237 |
+
world = bpy.context.scene.world
|
238 |
+
world.use_nodes = True
|
239 |
+
bg = world.node_tree.nodes['Background']
|
240 |
+
bg.inputs['Color'].default_value = (0, 0, 0, 1)
|
241 |
+
|
242 |
+
# Set up a better camera view
|
243 |
+
bpy.ops.object.camera_add(location=(0, -20, 15))
|
244 |
+
camera = bpy.context.active_object
|
245 |
+
camera.rotation_euler = (math.radians(55), 0, 0)
|
246 |
+
bpy.context.scene.camera = camera
|
247 |
+
|
248 |
+
# Add a sun light
|
249 |
+
bpy.ops.object.light_add(type='SUN', location=(10, -10, 10))
|
250 |
+
sun_light = bpy.context.active_object
|
251 |
+
sun_light.data.energy = 5.0
|
252 |
+
|
253 |
+
# Set render settings
|
254 |
+
bpy.context.scene.render.engine = 'CYCLES'
|
255 |
+
bpy.context.scene.cycles.device = 'CPU'
|
256 |
+
|
257 |
+
print('Solar system animation setup complete.')"""
|
258 |
+
}
|
259 |
+
|
260 |
+
# Add example selector
|
261 |
+
selected_example = st.sidebar.selectbox(
|
262 |
+
"Load example script:",
|
263 |
+
options=list(script_examples.keys())
|
264 |
+
)
|
265 |
+
|
266 |
# Sidebar: Blender script editor
|
267 |
st.sidebar.header("Blender Python Script")
|
268 |
script_text = st_ace(
|
269 |
+
value=script_examples[selected_example],
|
270 |
placeholder="Paste your Blender Python script here...",
|
271 |
language="python",
|
272 |
theme="monokai",
|
|
|
302 |
# Use a more robust temporary directory approach
|
303 |
tmp_dir = tempfile.mkdtemp(prefix="blender_app_")
|
304 |
try:
|
305 |
+
# 1) Preprocess and save user script
|
306 |
+
processed_script = preprocess_script(script_text, tmp_dir)
|
307 |
script_path = os.path.join(tmp_dir, "user_script.py")
|
308 |
with open(script_path, "w") as f:
|
309 |
+
f.write(processed_script)
|
310 |
|
311 |
# 2) Collect textures
|
312 |
texture_paths = []
|
|
|
498 |
- Generate and visualize 3D models with animations
|
499 |
- Download the results as GLB files for use in other applications
|
500 |
|
501 |
+
The app automatically adds necessary imports (`import os`) and file saving code to your scripts,
|
502 |
+
so you can focus on creating 3D content without worrying about environment details.
|
|
|
|
|
|
|
|
|
|
|
|
|
503 |
""")
|
504 |
|
505 |
with st.expander("Script Tips"):
|
|
|
515 |
image = bpy.data.images.load(texture_paths[0])
|
516 |
```
|
517 |
|
518 |
+
2. **You don't need to add file saving code** - the app automatically adds it
|
519 |
+
|
520 |
+
3. **Animations**: Make sure to set keyframes if you want your model to animate:
|
521 |
|
522 |
```python
|
523 |
+
# Example animation (rotate object 360 degrees over 250 frames)
|
524 |
+
obj.rotation_euler = (0, 0, 0)
|
525 |
+
obj.keyframe_insert(data_path="rotation_euler", frame=1)
|
526 |
+
obj.rotation_euler = (0, 0, math.radians(360))
|
527 |
+
obj.keyframe_insert(data_path="rotation_euler", frame=250)
|
528 |
```
|
|
|
|
|
529 |
""")
|