remesh / app.py
luca115's picture
QuadriFlow remesh; Make solid endpoint (/solid) with baked textures
981010f verified
Raw History Blame Contribute Delete
5.97 kB
"""
Remesh: clean quad or triangle topology for any 3D model, at the face count
you choose, with its textures baked onto the new mesh. Runs on CPU.
Made for https://3dvalley.com/tools/remesh, and usable on its own.
"""
import os
import tempfile
import time
import gradio as gr
from remesh import remesh
from solidify_server import DETAIL as SOLID_DETAIL
from solidify_server import solidify
MAX_BYTES = 150 * 1024 * 1024
TOPOLOGY = {"Quads": True, "Triangles": False}
HEADER = """
<div style="max-width:720px;margin:0 auto;text-align:center;padding:18px 16px 4px">
<h1 style="font-size:1.8rem;font-weight:700;margin:0 0 6px">Remesh</h1>
<p style="opacity:.7;margin:0">Clean quad or triangle topology for any 3D model, at the face count you choose,
with its textures baked onto the new mesh. Works on messy AI-generated models: holes, overlapping parts and
edges shared by three faces are sealed first, and the result always comes out closed, with no holes. Free, on CPU.</p>
</div>
"""
FOOTER = """
<div style="max-width:720px;margin:1.5rem auto .4rem;text-align:center;opacity:.65;font-size:.85rem">
Built on QuadriFlow, xatlas and trimesh. Part of the free 3D tools at
<a href="https://3dvalley.com/tools/remesh" target="_blank" rel="noopener">3dvalley.com</a>:
generate a model from a photo, repair it for printing, convert it, and more.
</div>
"""
def run(model_file, topology, target_faces, texture_size, progress=gr.Progress()):
if model_file is None:
raise gr.Error("Drop a model first: GLB, OBJ, STL or PLY.")
path = model_file if isinstance(model_file, str) else model_file.name
if os.path.getsize(path) > MAX_BYTES:
raise gr.Error("That file is over 150 MB. Reduce it first, or split it.")
out_dir = tempfile.mkdtemp()
stem = os.path.splitext(os.path.basename(path))[0] or "model"
glb = os.path.join(out_dir, f"{stem}-remeshed.glb")
obj = os.path.join(out_dir, f"{stem}-remeshed.obj")
started = time.perf_counter()
try:
info = remesh(
path,
glb,
obj,
target_faces=int(target_faces),
quads=TOPOLOGY.get(topology, True),
texture_size=int(texture_size),
progress=lambda fraction, text: progress(fraction, desc=text),
)
except ValueError as error:
raise gr.Error(str(error))
info["seconds"] = round(time.perf_counter() - started, 1)
return glb, [glb, obj], info
def run_solid(model_file, detail, progress=gr.Progress()):
"""Make solid: one closed, printable mesh with the most detail a CPU keeps, colour baked on."""
if model_file is None:
raise gr.Error("Drop a model first: GLB, OBJ, STL or PLY.")
path = model_file if isinstance(model_file, str) else model_file.name
if os.path.getsize(path) > MAX_BYTES:
raise gr.Error("That file is over 150 MB. Reduce it first, or split it.")
detail = str(detail or "standard").lower()
if detail not in SOLID_DETAIL:
raise gr.Error("Detail is standard or fine.")
out_dir = tempfile.mkdtemp()
stem = os.path.splitext(os.path.basename(path))[0] or "model"
glb = os.path.join(out_dir, f"{stem}-solid.glb")
stl = os.path.join(out_dir, f"{stem}-solid.stl")
try:
info = solidify(path, glb, stl, detail=detail, progress=lambda fraction, text: progress(fraction, desc=text))
except ValueError as error:
raise gr.Error(str(error))
return glb, [glb, stl], info
with gr.Blocks(title="Remesh: clean topology with baked textures") as demo:
gr.HTML(HEADER)
with gr.Row(equal_height=False):
with gr.Column():
model_file = gr.File(label="Model (GLB, OBJ, STL, PLY)")
topology = gr.Radio(list(TOPOLOGY), value="Quads", label="Topology", info="Quads for editing, sculpting and animation; triangles for games and the web.")
target_faces = gr.Slider(0, 100_000, value=0, step=500, label="Target faces", info="0 is Auto: the fewest faces that keep the shape.")
texture_size = gr.Radio([1024, 2048, 4096], value=2048, label="Texture size (px)")
button = gr.Button("Remesh", variant="primary")
with gr.Column():
preview = gr.Model3D(label="Result", clear_color=[0.08, 0.09, 0.11, 1])
files = gr.File(label="Download (GLB with textures, OBJ with quads)", file_count="multiple")
stats = gr.JSON(label="Details")
button.click(run, [model_file, topology, target_faces, texture_size], [preview, files, stats], api_name="remesh", concurrency_limit=1, concurrency_id="cpu")
with gr.Accordion("Make solid for 3D printing", open=False):
gr.Markdown(
"One closed, printable piece from any model: sealed in a fine voxel grid (512 or 768 across), "
"reduced to 300,000 or 600,000 triangles, snapped back onto the original for sharp edges, "
"and its colour baked into a texture. Download a GLB and an STL."
)
with gr.Row(equal_height=False):
with gr.Column():
solid_file = gr.File(label="Model (GLB, OBJ, STL, PLY)")
solid_detail = gr.Radio(list(SOLID_DETAIL), value="standard", label="Detail", info="Standard: 512 voxels across, 300k triangles. Fine: 768 across, 600k triangles, about twice as long.")
solid_button = gr.Button("Make solid", variant="primary")
with gr.Column():
solid_preview = gr.Model3D(label="Result", clear_color=[0.08, 0.09, 0.11, 1])
solid_files = gr.File(label="Download (GLB, STL)", file_count="multiple")
solid_stats = gr.JSON(label="Details")
solid_button.click(run_solid, [solid_file, solid_detail], [solid_preview, solid_files, solid_stats], api_name="solid", concurrency_limit=1, concurrency_id="cpu")
gr.HTML(FOOTER)
if __name__ == "__main__":
demo.queue(max_size=20).launch(ssr_mode=False, show_error=True)