"""Mako: collision hulls for Unreal (UCX_SM_Mako_Hull_NN in "MAKO | Collision (UCX)"), low-poly and convex, the Spearfish's way (models/tender-spearfish/build/sf_collision.py): * the hull in slices along its length, each the convex hull of rings of 16 points on the hull's exact shape at every 0.4 m or less and at both cut stations (shared, so no gaps), pushed 1 cm out; the last slice also takes the stern housing; * the parts that stand out of the skin (pods, big fins, dorsal, tail and ventral fins, the jaw's prongs, the belly pack, the clamp saddle with its clips): hulls of their own vertices outside the skin, cut to 24-48 points by farthest-point sampling, no loose or interior vertices; * the collection is hidden in the viewport (the eye in the outliner): drawn as wireframes over the model they look like a web of long thin triangles, which the user once took for the model's topology. Exec'd by mk_finish.py at the end of a build, or on its own in a built file: exec(open(r"...\\build\\mk_collision.py").read(), {"__name__": "ucx"})""" import bpy, bmesh, math, os from mathutils import Vector HERE = r"D:\HADAL - The Descent\models\tender-mako\build" if "profile" not in globals(): # standalone: the hull's shape (v04: section profiles) exec(open(os.path.join(HERE, "tender_lib.py")).read()) exec(open(os.path.join(HERE, "mk_measure.py")).read()) exec(open(os.path.join(HERE, "mk_shape.py")).read()) UCX_COLL = "MAKO | 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_Mako_Hull"] SRC_PTS = [src.matrix_world @ v.co for v in src.data.vertices] def fps(pts, k): """farthest-point sampling: k points spread over the set (keeps the extremes)""" 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 = [] def make(pts, k, tag): pts = hull_vertices(pts) pts = fps(pts, k) 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)}) # a vertex can be listed in both 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') bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) bm.normal_update() worst = max(max(f.normal.dot(v.co - f.verts[0].co) for v in bm.verts) for f in bm.faces) N_UCX[0] += 1 name = "UCX_SM_Mako_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 RING, PUSH = 16, 0.010 def ring_pts(y): if "BASE" in globals(): # the built grid: the station nearest y, its vertices pushed out i = min(range(NR), key=lambda q: abs(YS[q] - y)) return [BASE[i][k] + NRM[i][k] * PUSH for k in range(0, M, max(1, M // RING))] return [p + n * PUSH for p, n in section_pts(y, RING)] def slice_pts(a, b): """every grid vertex between a and b (the warp moves some of them along the hull), pushed out""" return [BASE[i][k] + NRM[i][k] * PUSH for i in range(NR) for k in range(0, M, 2) if a - 1e-4 <= BASE[i][k].y <= b + 1e-4] # v04: cuts at the bezel's faces (its shoulders are wider than the bubble ahead and the body behind) cuts = [Y_NOSE, -1.8, -1.2, -0.6, Y_B0 - 0.002, Y_B1 + 0.002, 0.9, 1.5, 2.2, 3.0, 3.8, Y_TAIL] for j in range(len(cuts) - 1): a, b = cuts[j], cuts[j + 1] n = max(1, int(math.ceil((b - a) / 0.4))) pts = [] for i in range(n + 1): y = a + (b - a) * i / n if y <= Y_NOSE + 0.02: pts.append(Vector((0, Y_NOSE, DZf(Y_NOSE)))) continue pts += ring_pts(y) if "BASE" in globals(): pts += slice_pts(a, b) if j == len(cuts) - 2: # the stern: the housing and its duct pts += [p for p in SRC_PTS if p.y > Y_TAIL - 0.04 and abs(p.x) < 0.30] make(pts, 64, "hull slice %.2f .. %.2f m" % (a, b)) def outside(p): return inside_depth(p) < -0.005 def _r_clamp(p): return math.hypot(p.x, p.y - Yi(CL_IMG)) def _near_clip(p, az): a = math.radians(az) cx, cy = 0.52 * math.cos(a), Yi(CL_IMG) + 0.52 * math.sin(a) return math.hypot(p.x - cx, p.y - cy) < 0.16 and p.z > 0.80 Y_P0, Y_P1 = Yi(POD4["x0"]) - 0.03, Yi(POD4["x1"]) + 0.03 PARTS = [ # v04 parts: side bodies, straight fins rooted on them, prongs, keel tooth, clips on fairings, the stern machine ("side body, port", 40, lambda p: p.x > 0.50 and -0.45 < p.z < 0.65 and Y_P0 < p.y < Y_P1), ("side body, starboard", 40, lambda p: p.x < -0.50 and -0.45 < p.z < 0.65 and Y_P0 < p.y < Y_P1), ("big fin, port", 32, lambda p: p.x > 0.70 and p.z < -0.25 and Yi(370.0) < p.y < Yi(665.0)), ("big fin, starboard", 32, lambda p: p.x < -0.70 and p.z < -0.25 and Yi(370.0) < p.y < Yi(665.0)), ("dorsal fin", 32, lambda p: abs(p.x) < 0.10 and p.z > 1.20 and Yi(390.0) < p.y < Yi(552.0)), ("tail fin", 24, lambda p: abs(p.x) < 0.08 and p.z > 0.40 and p.y > Yi(818.0)), ("ventral fin", 24, lambda p: abs(p.x) < 0.06 and p.z < -0.20 and p.y > Yi(826.0)), ("jaw prongs, port", 24, lambda p: 0.08 < p.x < 0.40 and p.z < -0.74 and Yi(235.0) < p.y < Yi(315.0)), ("jaw prongs, starboard", 24, lambda p: -0.40 < p.x < -0.08 and p.z < -0.74 and Yi(235.0) < p.y < Yi(315.0)), ("keel tooth", 16, lambda p: abs(p.x) < 0.11 and p.z < -0.80 and Yi(250.0) < p.y < Yi(305.0)), ("skids under the cockpit", 24, lambda p: abs(p.x) < 0.40 and p.z < -0.93 and Yi(325.0) < p.y < Yi(450.0)), ("belly pack", 24, lambda p: abs(p.x) < 0.36 and p.z < -0.15 and Yi(BELLY[0]) - 0.08 < p.y < Yi(BELLY[1]) + 0.08), ("holder clip 45", 16, lambda p: _near_clip(p, 45.0)), ("holder clip 135", 16, lambda p: _near_clip(p, 135.0)), ("holder clip 225", 16, lambda p: _near_clip(p, 225.0)), ("holder clip 315", 16, lambda p: _near_clip(p, 315.0)), ] 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 result = {"ucx": REPORT_UCX}