"""Grouper: collision hulls for Unreal (UCX_SM_Grouper_Hull_NN in "GROUPER | Collision (UCX)"), low-poly and convex, the Mako's and the Spearfish's way: * the hull in slices along its length, each the convex hull of the grid's vertices between its cuts (every 2nd column) pushed 1 cm out; the last slice takes the duct; * the parts that stand out of the skin: the collar, the dorsal ridge, the lamp heads, the sensor box, the scanner drum with its cradle, the stern fins, the skeg, the cheek fins and the bilge strakes: hulls of their own vertices outside the skin, cut to 16-48 points by farthest-point sampling; * the collection is hidden in the viewport (wireframes over the model read as a web of long triangles). Exec'd by gr_finish.py at the end of a build (needs the skin's points: SKIN_PTS, SKIN_NRM, round 3).""" import bpy, bmesh, math, os from mathutils import Vector UCX_COLL = "GROUPER | Collision (UCX)" col = bpy.data.collections.get(UCX_COLL) if col is None: col = bpy.data.collections.new(UCX_COLL) bpy.context.scene.collection.children.link(col) for o in list(col.objects): me_ = o.data bpy.data.objects.remove(o) if me_ is not None and me_.users == 0: bpy.data.meshes.remove(me_) src = bpy.data.objects["SM_Grouper_Hull"] SRC_PTS = [src.matrix_world @ v.co for v in src.data.vertices] def fps(pts, k): if len(pts) <= k: return list(pts) c = sum(pts, Vector()) / len(pts) chosen = [max(pts, key=lambda p: (p - c).length)] d = [(p - chosen[0]).length for p in pts] while len(chosen) < k: i = max(range(len(pts)), key=lambda j: d[j]) chosen.append(pts[i]) for j, p in enumerate(pts): dj = (p - pts[i]).length if dj < d[j]: d[j] = dj return chosen def hull_vertices(pts): bm_ = bmesh.new() vs = [bm_.verts.new(p) for p in pts] bmesh.ops.convex_hull(bm_, input=vs) out = [v.co.copy() for v in bm_.verts if v.link_faces] bm_.free() return out N_UCX = [0] REPORT_UCX = [] import random _RNG = random.Random(7) # seeded: the same hulls every build def _hull_bm(pts): bm_ = bmesh.new() vs = [bm_.verts.new(p) for p in pts] res = bmesh.ops.convex_hull(bm_, input=vs) junk = list({g for g in res["geom_interior"] + res["geom_unused"] if isinstance(g, bmesh.types.BMVert)}) if junk: bmesh.ops.delete(bm_, geom=junk, context='VERTS') loose = [v for v in bm_.verts if not v.link_faces] if loose: bmesh.ops.delete(bm_, geom=loose, context='VERTS') bm_.normal_update() worst = max(min(max(f.normal.dot(v.co - f.verts[0].co) for v in bm_.verts), max(-f.normal.dot(v.co - f.verts[0].co) for v in bm_.verts)) for f in bm_.faces) return bm_, worst def make(pts, k, tag): pts = hull_vertices(pts) pts = fps(pts, k) # bmesh's convex hull can come out slightly non-convex on near-coplanar points: retry with a 0.1 mm jitter, keep the best best = None for attempt in range(6): jit = [p + Vector((_RNG.uniform(-1, 1), _RNG.uniform(-1, 1), _RNG.uniform(-1, 1))) * (0.0 if attempt == 0 else 0.0001 * attempt) for p in pts] cand = _hull_bm(jit) if best is None or cand[1] < best[1]: if best is not None: best[0].free() best = cand else: cand[0].free() if best[1] < 0.0005: break bm_ = best[0] pts = [v.co.copy() for v in bm_.verts] bm_.free() bm_ = bmesh.new() vs = [bm_.verts.new(p) for p in pts] res = bmesh.ops.convex_hull(bm_, input=vs) junk = list({g for g in res["geom_interior"] + res["geom_unused"] if isinstance(g, bmesh.types.BMVert)}) if junk: bmesh.ops.delete(bm_, geom=junk, context='VERTS') loose = [v for v in bm_.verts if not v.link_faces] if loose: bmesh.ops.delete(bm_, geom=loose, context='VERTS') bm_.normal_update() cen = sum((v.co for v in bm_.verts), Vector()) / len(bm_.verts) flip = [f for f in bm_.faces if f.normal.dot(f.calc_center_median() - cen) < 0] if flip: bmesh.ops.reverse_faces(bm_, faces=flip) bm_.normal_update() worst = max(min(max(f.normal.dot(v.co - f.verts[0].co) for v in bm_.verts), max(-f.normal.dot(v.co - f.verts[0].co) for v in bm_.verts)) for f in bm_.faces) # each face's plane with every vertex on one side (orientation aside) N_UCX[0] += 1 name = "UCX_SM_Grouper_Hull_%02d" % N_UCX[0] me_ = bpy.data.meshes.new(name) bm_.to_mesh(me_) bm_.free() o = bpy.data.objects.new(name, me_) col.objects.link(o) o.display_type = 'WIRE' o.hide_render = True REPORT_UCX.append(dict(name=name, part=tag, verts=len(me_.vertices), tris=sum(len(p.vertices) - 2 for p in me_.polygons), convex_within_mm=round(max(0.0, worst) * 1000, 2))) return name PUSH = 0.010 cuts = [Y_NOSE, -2.55, -2.2, -1.85, -1.5, -1.15, -0.8, -0.45, -0.1, 0.25, 0.6, 0.95, 1.3, 1.65, 2.0, 2.4, 2.8, 3.2, Y_TAIL] for j in range(len(cuts) - 1): a, b = cuts[j], cuts[j + 1] pts = [p_ + n_ * PUSH for p_, n_ in zip(SKIN_PTS, SKIN_NRM) if a - 1e-4 <= p_.y <= b + 1e-4] # (round 3) the skin's points if j == 0: pts += [p for p in SRC_PTS if p.y < Y_NOSE + 0.005 and p.z > -0.6] # the chin's closing rings if j == len(cuts) - 2: pts += [p for p in SRC_PTS if p.y > Y_TAIL - 0.01 and math.hypot(p.x, p.z - DUCT_AX.z) > 0.5] # the duct make(pts, 64, "hull slice %.2f .. %.2f m" % (a, b)) def outside(p): return inside_depth(p) < -0.004 def box_sel(x0, x1, y0, y1, z0, z1): return lambda p: x0 <= p.x <= x1 and y0 <= p.y <= y1 and z0 <= p.z <= z1 r_col = lambda p: math.hypot(p.x - CL_C.x, p.y - CL_C.y) PARTS = [ ("collar", 48, lambda p: r_col(p) < 0.70 and p.z > 1.10), ("dorsal ridge", 24, box_sel(-0.25, 0.25, Yi(630.0), Yi(548.0), 0.6, 1.2)), ("lamp head, port", 24, box_sel(0.30, 0.70, Yi(864.0), Yi(841.0), 0.85, 1.35)), ("lamp head, starboard", 24, box_sel(-0.70, -0.30, Yi(864.0), Yi(841.0), 0.85, 1.35)), ("sensor box", 16, box_sel(-0.12, 0.12, Yi(860.0), Yi(848.0), 1.0, 1.3)), ("scanner drum and cradle", 48, box_sel(-0.70, 0.70, Yi(952.0), Yi(870.0), -1.5, -0.25)), ("stern fin, port", 24, box_sel(0.70, 1.20, Yi(608.0), Yi(572.0), -0.40, 0.25)), ("stern fin, starboard", 24, box_sel(-1.20, -0.70, Yi(608.0), Yi(572.0), -0.40, 0.25)), ("skeg", 24, box_sel(-0.20, 0.20, Yi(616.0), Yi(544.0), -1.40, -0.50)), ("cheek fin, port", 16, box_sel(0.60, 1.20, Yi(868.0), Yi(846.0), -1.4, -0.5)), ("cheek fin, starboard", 16, box_sel(-1.20, -0.60, Yi(868.0), Yi(846.0), -1.4, -0.5)), ] for tag, k, sel in PARTS: pts = [p for p in SRC_PTS if sel(p) and outside(p)] if len(pts) >= 4: make(pts, k, tag) lc = bpy.context.view_layer.layer_collection.children.get(UCX_COLL) if lc is not None: lc.hide_viewport = True