File size: 5,967 Bytes
21bf454
 
 
 
 
 
 
 
 
 
 
 
 
 
981010f
 
21bf454
 
 
 
 
 
 
 
 
981010f
21bf454
 
 
 
 
981010f
21bf454
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981010f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21bf454
 
 
 
 
 
260d22c
21bf454
 
 
 
 
 
981010f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21bf454
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""
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)