Download app.py from Upsampler/remesh: direct link, hf CLI and curl.
- Browser
- Download file 5.97 kB
-
https://huggingface.co/spaces/Upsampler/remesh/resolve/main/app.py
- Command line
-
hf download hf://spaces/Upsampler/remesh/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Upsampler/remesh/resolve/main/app.py
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) | |