Create streamlit application which take a OBJ file in input, and display the 3d mesh in a 3D plotly chart
To upload files, please first save the app
import streamlit as st
import plotly.graph_objects as go
import numpy as np
from pathlib import Path
class Mesh:
def __init__(self):
self.vertices = []
self.faces = []
self.name = ""
self.color = None # Will be assigned randomly
def generate_random_color():
"""Generate a random color in RGB format"""
return f'rgb({np.random.randint(0, 256)}, {np.random.randint(0, 256)}, {np.random.randint(0, 256)})'
def parse_obj(obj_content):
meshes = []
current_mesh = None
vertices = []
for line in obj_content.decode('utf-8').split('\n'):
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.split()
if not parts:
continue
if parts[0] == 'o' or parts[0] == 'g': # New mesh/group
if current_mesh and current_mesh.faces: # Save previous mesh if it has faces
meshes.append(current_mesh)
current_mesh = Mesh()
current_mesh.name = parts[1]
current_mesh.color = generate_random_color()
elif parts[0] == 'v': # Vertex
vertices.append([float(x) for x in parts[1:4]])
elif parts[0] == 'f': # Face
if current_mesh is None:
current_mesh = Mesh()
current_mesh.name = f"mesh_{len(meshes)}"
current_mesh.color = generate_random_color()
# Handle different face formats (v, v//vn, v/vt/vn)
face_vertices = []
for vert in parts[1:]:
# Get only the vertex index (before any '/' character)
vertex_idx = int(vert.split('/')[0]) - 1
face_vertices.append(vertex_idx)
current_mesh.faces.append(face_vertices)
# Add the last mesh if it has faces
if current_mesh and current_mesh.faces:
meshes.append(current_mesh)
# Convert vertices to numpy array and assign to all meshes
vertices = np.array(vertices)
for mesh in meshes:
mesh.vertices = vertices
return meshes
def create_combined_mesh_plot(meshes, visible_meshes):
fig = go.Figure()
# Calculate overall bounds for centering
all_vertices = np.concatenate([mesh.vertices for mesh in meshes])
center = np.mean(all_vertices, axis=0)
max_range = np.max(np.max(all_vertices, axis=0) - np.min(all_vertices, axis=0))
for mesh_idx, mesh in enumerate(meshes):
# Convert faces to triangles
triangles_i = []
triangles_j = []
triangles_k = []
for face in mesh.faces:
if len(face) == 3: # Triangle
triangles_i.append(face[0])
triangles_j.append(face[1])
triangles_k.append(face[2])
elif len(face) == 4: # Quad - split into two triangles
triangles_i.extend([face[0], face[0]])
triangles_j.extend([face[1], face[2]])
triangles_k.extend([face[2], face[3]])
# Add mesh to plot
fig.add_trace(go.Mesh3d(
x=mesh.vertices[:, 0],
y=mesh.vertices[:, 1],
z=mesh.vertices[:, 2],
i=triangles_i,
j=triangles_j,
k=triangles_k,
name=mesh.name,
color=mesh.color,
opacity=0.8,
visible=mesh_idx in visible_meshes,
showscale=False
))
# Update layout for better visualization
fig.update_layout(
scene=dict(
aspectmode='cube', # Force equal aspect ratio
camera=dict(
up=dict(x=0, y=1, z=0),
center=dict(x=0, y=0, z=0),
eye=dict(x=1.5, y=1.5, z=1.5)
),
xaxis=dict(range=[center[0] - max_range/2, center[0] + max_range/2]),
yaxis=dict(range=[center[1] - max_range/2, center[1] + max_range/2]),
zaxis=dict(range=[center[2] - max_range/2, center[2] + max_range/2])
),
title='3D Mesh Visualization',
width=800,
height=800,
showlegend=True,
legend=dict(
yanchor="top",
y=0.99,
xanchor="left",
x=0.01
)
)
return fig
# Streamlit app
st.set_page_config(layout="wide")
st.title("Multi-Mesh OBJ Viewer")
# File uploader for multiple OBJ files
uploaded_files = st.file_uploader("Choose OBJ files", type=['obj'], accept_multiple_files=True)
if uploaded_files:
try:
# Process all uploaded files
all_meshes = []
for uploaded_file in uploaded_files:
meshes = parse_obj(uploaded_file.read())
# Add file name to mesh names for clarity
file_name = Path(uploaded_file.name).stem
for mesh in meshes:
mesh.name = f"{file_name}: {mesh.name}"
all_meshes.extend(meshes)
# Create columns for statistics and controls
col1, col2 = st.columns([2, 1])
with col1:
st.header("Mesh Statistics")
stats_cols = st.columns(min(len(all_meshes), 3))
for i, mesh in enumerate(all_meshes):
with stats_cols[i % 3]:
st.metric(
f"Mesh: {mesh.name}",
f"{len(mesh.faces)} faces",
f"{len(mesh.vertices)} vertices"
)
# Mesh visibility controls
with col2:
st.header("Visibility Controls")
visible_meshes = []
for i, mesh in enumerate(all_meshes):
if st.checkbox(f"Show {mesh.name}", value=True, key=f"mesh_{i}"):
visible_meshes.append(i)
# Additional controls
st.sidebar.header("Visualization Controls")
opacity = st.sidebar.slider("Opacity", 0.0, 1.0, 0.8)
# Create and display the combined visualization
fig = create_combined_mesh_plot(all_meshes, visible_meshes)
# Update opacity for all meshes
for trace in fig.data:
trace.opacity = opacity
# Display the plot
st.plotly_chart(fig, use_container_width=True)
except Exception as e:
st.error(f"Error processing the OBJ file(s): {str(e)}")
st.write("Please make sure all files are valid OBJ files.")
else:
st.info("Please upload one or more OBJ files to visualize them.")
# Example of expected file format
st.markdown("""
### Expected OBJ File Format:
```
o mesh_name # Object name (optional)
v x1 y1 z1 # Vertex
v x2 y2 z2
v x3 y3 z3
...
f v1 v2 v3 # Face (triangle)
f v1 v2 v3 v4 # Face (quad)
```
""")
Hi! I can help you with any questions about Streamlit and Python. What would you like to know?