# Stage 30b: repair face winding on the decimated GLB, in place. # # blender --background --python 30b_fix_winding.py -- # # Decimation re-triangulates, and it reintroduced 61 inward-facing triangles on the 32k mesh — # the same defect stage 25 cleared from the hires sculpt (1511 -> 10). A backfacing triangle # renders black, so handing the retexture/re-atlas lane a mesh speckled with them would waste # their pass. Same two-step remedy: propagate consistent orientation, then reverse whatever the # non-manifold edges blocked (make_consistent cannot cross them). No vertex moves. import bpy, bmesh, sys, os, time import numpy as np argv = sys.argv[sys.argv.index("--") + 1:] SRC, OUT = argv[0], argv[1] t0 = time.time() def log(m): print(f"[wind {time.time()-t0:6.1f}s] {m}", flush=True) bpy.ops.wm.read_homefile(use_empty=True) bpy.ops.import_scene.gltf(filepath=SRC) ob = max([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: len(o.data.vertices)) me = ob.data bpy.context.view_layer.objects.active = ob for o in bpy.data.objects: o.select_set(o is ob) log(f"in: {len(me.vertices)}v {len(me.polygons)}f") def flipped_mask(): n_f = len(me.polygons) fn = np.empty(n_f * 3) me.polygons.foreach_get("normal", fn) fn = fn.reshape(-1, 3) n_v = len(me.vertices) vv = np.empty(n_v * 3) me.vertices.foreach_get("normal", vv) vv = vv.reshape(-1, 3) lv = np.empty(len(me.loops), dtype=np.int32) me.loops.foreach_get("vertex_index", lv) ls = np.empty(n_f, dtype=np.int32) me.polygons.foreach_get("loop_start", ls) lt = np.empty(n_f, dtype=np.int32) me.polygons.foreach_get("loop_total", lt) acc = np.zeros((n_f, 3)) for k in range(int(lt.max())): sel = lt > k acc[sel] += vv[lv[ls[sel] + k]] acc /= np.maximum(np.linalg.norm(acc, axis=1, keepdims=True), 1e-12) return (fn * acc).sum(axis=1) < 0 print(f"FLIPPED before: {int(flipped_mask().sum())} of {len(me.polygons)}") bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') me.update() print(f"FLIPPED after make_consistent: {int(flipped_mask().sum())}") for rnd in range(4): bad = np.nonzero(flipped_mask())[0] if len(bad) == 0: log(f"round {rnd}: none left") break bm = bmesh.new() bm.from_mesh(me) bm.faces.ensure_lookup_table() bmesh.ops.reverse_faces(bm, faces=[bm.faces[int(i)] for i in bad]) bm.to_mesh(me) bm.free() me.update() log(f"round {rnd}: reversed {len(bad)}") print(f"FLIPPED final: {int(flipped_mask().sum())} of {len(me.polygons)}") vn = np.empty(len(me.vertices) * 3, dtype=np.float32) me.vertices.foreach_get("normal", vn) if me.has_custom_normals: me.normals_split_custom_set_from_vertices(vn.reshape(-1, 3)) bpy.ops.export_scene.gltf(filepath=OUT, export_format='GLB', export_image_format='AUTO', export_yup=True, export_apply=False, export_animations=False, export_skins=False, export_morph=False) log(f"WROTE {OUT} ({os.path.getsize(OUT)/1e6:.2f} MB)") print("WIND_DONE")