diff --git a/CHANGELOG.md b/CHANGELOG.md index 5512489..387811e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to Splat Workshop. The format follows [Keep a Changelog](htt ### Added - **Mesh export**: *Mesh …* on every scene and tour turns the splats into a surface with Spirula Studio (`spirula mesh`) – for Blender and other 3D programs, or for 3D printing. Formats: **GLB** (recommended: colours or texture in one file), **STL** (no colours, for slicers), **OBJ** (with a texture if possible, otherwise without colours) and **PLY** (colours per corner). - - **Stands upright, floor at height 0**: our scenes have Y pointing down like the Steam Frame's tracking, so a mesh used to lie on its side in Blender. Now OBJ, PLY and STL get Z up and GLB gets Y up, as the glTF standard wants (Blender turns that into Z up itself); the floor – found as for the floor plan – is moved to height 0. In metres. + - **Stands upright, floor at height 0**: our scenes have Y pointing down like the Steam Frame's tracking, so a mesh used to lie on its side in Blender. Now PLY and STL get Z up, GLB and OBJ Y up as their standard and custom want (Blender turns those into Z up itself on import – checked in Blender 5.2 for every format); the floor – found as for the floor plan – is moved to height 0. In metres. - What lies well outside the room is left out before meshing (as the *Edges* clean-up does) – otherwise big blobs beyond the walls become lumps floating around the room. - A **texture** from the training images when they are still there; after *clean up → training images*, colours per corner from the splats. - The mesh lives in the scene's folder (`mesh/`); a new one replaces the old one. *Mesh ✓* shows which scenes have one. diff --git a/scripts/mesh.sh b/scripts/mesh.sh index d6d128a..0e37c43 100644 --- a/scripts/mesh.sh +++ b/scripts/mesh.sh @@ -7,7 +7,7 @@ # comma-separated: glb, ply, obj, stl # glb colours (texture or per corner); Y up as glTF wants it – Blender, Windows 3D viewer, web # ply colours per corner; Z up – MeshLab, CloudCompare, Blender -# obj with a texture, or without colours; Z up – nearly every 3D program +# obj with a texture, or without colours; Y up as is custom for OBJ – nearly every 3D program # stl no colours; Z up – slicers for 3D printing # texture 1: a texture from the training images (only if they are still there, see "clean up"), # otherwise colours per corner from the splats diff --git a/scripts/upright.py b/scripts/upright.py index 9929585..73249ec 100644 --- a/scripts/upright.py +++ b/scripts/upright.py @@ -1,10 +1,12 @@ -"""Stand a mesh upright: our scenes have Y pointing down (like the Steam Frame's tracking); 3D programs -expect Z up (OBJ, PLY, STL – e.g. Blender, slicers for 3D printing) or Y up (glTF/GLB, by its standard; -Blender turns that into Z up itself on import). Without this a room lies on its side or upside down. +"""Stand a mesh upright: our scenes have Y pointing down (like the Steam Frame's tracking). 3D programs +expect Z up for PLY and STL (Blender, slicers for 3D printing) and Y up for glTF/GLB (by its standard) and +OBJ (by custom) – Blender turns those two into Z up itself on import. Without this a room lies on its side +or upside down. (Checked by importing each format in Blender 5.2.) Usage: python3 upright.py [--floor Y] [ ...] (.ply .obj .stl .glb .gltf; changed in place) - OBJ, PLY, STL: positions and normals turned so that up (-Y) becomes +Z: (x, y, z) -> (x, z, -y) + PLY, STL: positions and normals turned so that up (-Y) becomes +Z: (x, y, z) -> (x, z, -y) + OBJ: turned 180° about X, up (-Y) becomes +Y: (x, y, z) -> (x, -y, -z) GLB/glTF: the scene's root nodes go under one new node turned 180° about X: (x, y, z) -> (x, -y, -z) – nothing else in the file is touched (textures, materials, accessors stay as they are) --floor Y where the floor is in the scene (Y down): it is moved to height 0, so the room stands on @@ -67,8 +69,8 @@ def obj(path: Path): tag, rest = line.split(None, 1) nums = rest.split() x, y, z = (float(n) for n in nums[:3]) - x, y, z = Z_UP @ (x, y, z) - z += LIFT if tag == b"v" else 0.0 + x, y, z = x, -y, -z # Y up (180° about X) + y += LIFT if tag == b"v" else 0.0 line = b"%s %.7g %.7g %.7g%s\n" % (tag, x, y, z, (b" " + b" ".join(nums[3:])) if nums[3:] else b"") out.append(line) _replace(path, b"".join(out)) diff --git a/tests/test_mesh.py b/tests/test_mesh.py index de2a156..6c52a02 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -23,12 +23,12 @@ def test_upright_puts_the_floor_at_the_bottom(tmp_path, fmt, colour): r = subprocess.run([sys.executable, str(UPRIGHT), "--floor", str(fake_mesh.FLOOR_Y), str(f)], capture_output=True, text=True) assert r.returncode == 0, r.stderr p = fake_mesh.read_points(f) - up = 1 if fmt == "glb" else 2 # glTF: Y up; the others: Z up + up = 1 if fmt in ("glb", "obj") else 2 # glTF and OBJ: Y up; PLY and STL: Z up (as Blender imports them) k = 6 if fmt == "stl" else 4 # STL lists the corners of each triangle: 2 floor triangles floor, ceiling = p[:k, up], p[k:, up] assert np.allclose(floor, 0, atol=1e-5) # the floor at height 0 ... assert np.allclose(ceiling, fake_mesh.FLOOR_Y - fake_mesh.CEILING_Y, atol=1e-5) # ... the ceiling 2.5 m above - if fmt in ("obj", "ply"): # a proper turn, not a mirror: x stays, the other two swap + if fmt in ("obj", "ply"): # a proper turn, not a mirror: x stays assert np.allclose(p[:, 0], fake_mesh.V[:, 0]) if fmt == "ply": # normals turn along: the floor faces up raw = f.read_bytes() diff --git a/web/i18n.js b/web/i18n.js index 5ee5d29..6908a84 100644 --- a/web/i18n.js +++ b/web/i18n.js @@ -270,7 +270,7 @@ const I18N = { "ver.recommended": "(recommended)", "mesh.button": "Mesh …", "mesh.button.has": "Mesh ✓", "mesh.button.title": "A mesh (surface) for 3D programs and 3D printing", "mesh.title": "Mesh of “{name}”", - "mesh.hint": "Spirula Studio turns the splats into a surface – for Blender and other 3D programs or for 3D printing. It stands upright (Z or, for GLB, Y up) with the floor at height 0; what lies well outside the room is left out. In metres.", + "mesh.hint": "Spirula Studio turns the splats into a surface – for Blender and other 3D programs or for 3D printing. It stands upright (the way Blender & co. expect each format) with the floor at height 0; what lies well outside the room is left out. In metres.", "mesh.new": "New mesh", "mesh.have": "Mesh", "mesh.make": "Make mesh", "mesh.pick": "Pick at least one format", "mesh.started": "Making the mesh …", "mesh.delete": "Delete the mesh", "mesh.confirm": "Move the mesh of “{name}” to the trash?", @@ -567,7 +567,7 @@ const I18N = { "ver.recommended": "(empfohlen)", "mesh.button": "Mesh …", "mesh.button.has": "Mesh ✓", "mesh.button.title": "Ein Mesh (Oberfläche) für 3D-Programme und den 3D-Druck", "mesh.title": "Mesh von „{name}“", - "mesh.hint": "Spirula Studio macht aus den Splats eine Oberfläche – für Blender und andere 3D-Programme oder für den 3D-Druck. Sie steht aufrecht (Z oben, bei GLB Y oben) mit dem Boden auf Höhe 0; was deutlich außerhalb des Raums liegt, bleibt weg. In Metern.", + "mesh.hint": "Spirula Studio macht aus den Splats eine Oberfläche – für Blender und andere 3D-Programme oder für den 3D-Druck. Sie steht aufrecht (so, wie Blender & Co. jedes Format erwarten) mit dem Boden auf Höhe 0; was deutlich außerhalb des Raums liegt, bleibt weg. In Metern.", "mesh.new": "Neues Mesh", "mesh.have": "Mesh", "mesh.make": "Mesh erstellen", "mesh.pick": "Mindestens ein Format wählen", "mesh.started": "Erstelle das Mesh …", "mesh.delete": "Mesh löschen", "mesh.confirm": "Das Mesh von „{name}“ in den Papierkorb verschieben?",