"""Repair Studio face winding on welded topology without generating UVs.""" import json import sys from pathlib import Path import bpy import bmesh source, destination, kind = sys.argv[sys.argv.index('--') + 1:][:3] bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.gltf(filepath=source, merge_vertices=False) obj = next(o for o in bpy.context.scene.objects if o.type == 'MESH') bpy.context.view_layer.objects.active = obj bpy.ops.object.select_all(action='DESELECT'); obj.select_set(True) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) uv = obj.data.uv_layers.active uv_before = sorted(tuple(round(c, 6) for c in v.uv) for v in uv.data) bm = bmesh.new(); bm.from_mesh(obj.data) bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=max(obj.dimensions) * 1e-7) bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces)) flipped = 0 if kind == 'ground': # Thin terrain is intentionally authored with a top and underside. Some # Studio patches are disconnected, so orient each flat patch explicitly. low = min(v.co.z for v in bm.verts); high = max(v.co.z for v in bm.verts) midpoint = (low + high) / 2 for face in bm.faces: face.normal_update() if abs(face.normal.z) > .35: # Terrain is viewed and collided from above. Its paving can be # lower than raised grass, so a global height midpoint cannot # distinguish the paving top from an underside patch. if face.normal.z < 0: face.normal_flip(); flipped += 1 bm.to_mesh(obj.data); bm.free(); obj.data.update() uv_after = sorted(tuple(round(c, 6) for c in v.uv) for v in obj.data.uv_layers.active.data) assert uv_before == uv_after, 'UV coordinates changed during winding repair' bpy.ops.export_scene.gltf(filepath=destination, export_format='GLB', use_selection=True, export_materials='EXPORT', export_image_format='AUTO') report = dict(kind=kind, uvPreserved=True, flatTerrainFacesFlipped=flipped, triangles=sum(len(p.vertices)-2 for p in obj.data.polygons)) Path(destination).with_suffix('.json').write_text(json.dumps(report, indent=2), encoding='utf-8') print('MT_SUMMARY '+json.dumps(report), flush=True)