Files
AIGame/Tools/model_tools/bl_decimate_homecamp_uv.py

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)