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

Remesh

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.

""" FOOTER = """
Built on QuadriFlow, xatlas and trimesh. Part of the free 3D tools at 3dvalley.com: generate a model from a photo, repair it for printing, convert it, and more.
""" 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)