82 lines
3.5 KiB
Python
82 lines
3.5 KiB
Python
"""Weld coincident geometry, retain corner UV seams, then collapse to target.
|
|
|
|
Studio GLBs split vertices at normals and UV seams. Decimating those as loose
|
|
triangles drops patches rather than simplifying a continuous surface.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
import bmesh
|
|
|
|
source, target, requested = sys.argv[sys.argv.index('--') + 1:][:3]
|
|
source, target, requested = Path(source), Path(target), int(requested)
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
bpy.ops.import_scene.gltf(filepath=str(source), merge_vertices=False)
|
|
objects = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
|
assert len(objects) == 1
|
|
obj = objects[0]
|
|
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)
|
|
assert obj.data.uv_layers.active
|
|
|
|
|
|
def corners():
|
|
# Reordering by bmesh is harmless; the (position, per-corner UV) multiset
|
|
# must stay the same through welding, including UV seam discontinuities.
|
|
uv = obj.data.uv_layers.active.data
|
|
return sorted(tuple(round(c, 6) for c in (*obj.data.vertices[l.vertex_index].co, *uv[l.index].uv)) for l in obj.data.loops)
|
|
|
|
|
|
before_corners = corners()
|
|
vertices_before = len(obj.data.vertices)
|
|
bm = bmesh.new()
|
|
bm.from_mesh(obj.data)
|
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=max(obj.dimensions) * 1e-7)
|
|
bm.to_mesh(obj.data)
|
|
bm.free()
|
|
obj.data.update()
|
|
assert before_corners == corners(), 'Welding changed the existing corner UV mapping'
|
|
vertices_welded = len(obj.data.vertices)
|
|
tri = obj.modifiers.new('Triangulate', 'TRIANGULATE')
|
|
bpy.ops.object.modifier_apply(modifier=tri.name)
|
|
before = len(obj.data.polygons)
|
|
area_before = sum(p.area for p in obj.data.polygons)
|
|
modifier = obj.modifiers.new('PreserveCornerUVDecimate', 'DECIMATE')
|
|
modifier.decimate_type = 'COLLAPSE'
|
|
modifier.use_collapse_triangulate = True
|
|
lower, upper = 0.0, 1.0
|
|
candidate = min(1.0, requested / before)
|
|
best = None
|
|
for _ in range(22):
|
|
modifier.ratio = candidate
|
|
bpy.context.view_layer.update()
|
|
count = len(obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).data.polygons)
|
|
rank = (abs(count - requested), count > requested)
|
|
if best is None or rank < best[0]: best = (rank, candidate)
|
|
if count == requested: break
|
|
if count > requested: upper = candidate
|
|
else: lower = candidate
|
|
candidate = (lower + upper) / 2
|
|
modifier.ratio = best[1]
|
|
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
|
after = len(obj.data.polygons)
|
|
area_after = sum(p.area for p in obj.data.polygons)
|
|
print('Final faces', after, 'target', requested, 'best', best, flush=True)
|
|
assert after <= requested * 1.30, (after, requested, best)
|
|
assert area_after >= area_before * .80, 'Reduction lost too much surface area'
|
|
assert obj.data.uv_layers.active
|
|
bpy.ops.export_scene.gltf(filepath=str(target), export_format='GLB', use_selection=True,
|
|
export_materials='EXPORT', export_image_format='AUTO')
|
|
report = dict(faces_before=before, faces_after=after, target_faces=requested,
|
|
vertices_before=vertices_before, vertices_after_weld=vertices_welded,
|
|
cornerUVPreservedDuringWeld=True, uv_mode='preserved_no_unwrap',
|
|
area_before=area_before, area_after=area_after, area_ratio=area_after / area_before,
|
|
ratio=best[1], threshold_percent=30, triggered=True)
|
|
target.with_suffix('.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
|
|
print('MT_SUMMARY ' + json.dumps(report), flush=True)
|