# Mako: the hull, its openings, glass, lining and cockpit (exec'd by build_mako.py after mk_shape.py). # # Topology (the Spearfish's rebuilt method, models/tender-spearfish/build/sf_hull.py): # * one quad grid: 64 columns round, placed by arc length on each section, stations spaced by need; # * every opening is a region of whole grid faces: columns are pinned to the measured outlines (the glass's lower edge, # the hood's edge, the shell's edge, the window, the hatch) and relax back to their even places within 0.35 m past # the ends, so no vertex is snapped and nothing is cut; # * the canopy is one region: the whole nose ahead of the jaw, then everything above the chin line, then (under the hood) # the band between the chin line and the hood's edge, up to the collar. Its boundary is one closed loop of grid edges; # round it a frame in the hull's own topology (one row of faces, inner edge raised 10 mm) and a sleeve down to the # lining. The glass is the region's own grid sunk 22 mm (less toward the nose point, which is glass); # * the collar is a raised band of the grid (35 mm, 8 mm as a rib under the jaw) between two pairs of stations 6 mm # apart (its front and back faces); aft of it the orange shell stands 10 mm proud of the dark lower hull on a pair of # columns 3 mm apart (the step), so the plating overlap is real geometry, not paint; # * the hatch under the clamp saddle is a square of grid faces with a sleeve to the lining (the saddle's trunk passes it); # * vertex layers: seam_ring (panel seams, 5 + signed distance, 0 = none) for the surfaces pass, and the analytic normal # (an_x, an_y, an_z) with a face flag (on_skin) so the finish pass can give the skin smooth custom normals. exec(open(os.path.join(HERE, "mk_topo.py")).read()) M = 64 HALF = M // 2 # port columns j = 0 (keel) .. 32 (top), starboard mirrored: k = 64 - j J_CH = 9 # chin line: glass above, the jaw below; the sill frame on 8..9 J_HE = 24 # hood edge (glass below, hood above, frame 24..25); aft of the collar the shell's step 24..25 J_WB, J_WT = 12, 16 # rear bay window band, frames 11..12 and 16..17 J_HH = 28 # hatch hole over the top from column 28, frame 27..28 (hidden under the saddle) FR_W, FR_H = 0.045, 0.040 # frame widths (arc on the skin) RIM_W, RIM_H = 0.010, 0.006 # how far a frame's inner edge stands proud GLASS_D, LINER = 0.022, 0.060 RELAX = 0.35 H_COL, H_COL_LOW = 0.035, 0.008 # collar band, and its rib under the jaw H_SHELL = 0.010 # the shell's plating over the lower hull STEP = 0.006 # station gap of the collar's front and back faces NSEC = 360 Y_JAW, Y_HOOD = Yi(X_JAW), Yi(X_HOOD) Y_C0, Y_C1 = Yi(X_COL0), Yi(X_COL1) Y_CL = Yi(CL_IMG) # the clamp axis HATCH_R = 0.30 HY_A, HY_B = Y_CL - HATCH_R, Y_CL + HATCH_R Y_BULK = Yi(352.0) # cockpit's rear bulkhead Y_BAY = Yi(800.0) # the rear bay's end (the lining's cap) def half_table(y): th = [-math.pi / 2 + math.pi * i / NSEC for i in range(NSEC + 1)] pts = [sec(y, t) for t in th] arc = [0.0] for i in range(1, len(pts)): arc.append(arc[-1] + (pts[i] - pts[i - 1]).length) return th, arc def interp(xs, ys, x): if x <= xs[0]: return ys[0] if x >= xs[-1]: return ys[-1] lo, hi = 0, len(xs) - 1 while hi - lo > 1: mid = (lo + hi) // 2 if xs[mid] <= x: lo = mid else: hi = mid t = (x - xs[lo]) / (xs[hi] - xs[lo]) return ys[lo] + (ys[hi] - ys[lo]) * t def theta_of_z(y, z): """port flank angle of the point at height z""" zm = ZMf(y); top = z >= zm n = NT(y) if top else NB(y); H = HTf(y) if top else HBf(y) q = min(1.0, abs(z - zm) / H) ** (n / 2.0) return math.copysign(math.asin(min(1.0, q)), z - zm) def theta_of_lat(y, lat): """angle of the point on the top at lateral offset lat""" n = NT(y); W = Wf(y) c = min(1.0, abs(lat) / W) ** (n / 2.0) return math.acos(min(1.0, c)) def side_span(poly, y): zs = [] n = len(poly) for i in range(n): (y1, z1), (y2, z2) = poly[i], poly[(i + 1) % n] if (y1 - y) * (y2 - y) <= 0 and y1 != y2: zs.append(z1 + (z2 - z1) * (y - y1) / (y2 - y1)) return (min(zs), max(zs)) if len(zs) >= 2 else None def opening_range(poly, hmin=0.06): ys = [p[0] for p in poly] a, b = min(ys), max(ys) ok = [] for i in range(801): y = a + (b - a) * i / 800 s = side_span(poly, y) if s and s[1] - s[0] >= hmin: ok.append(y) return min(ok), max(ok) WIN = dict(poly=P_SIDEWIN) WIN["ya"], WIN["yb"] = opening_range(P_SIDEWIN) def weight(y, a, b, pad): if a <= y <= b: return 1.0 d = (a - y) if y < a else (y - b) return 1.0 - smoothstep(pad, pad + RELAX, d) def edge_theta(y): """the orange's edge on the flank: the hood's edge ahead of the collar, the shell's lower edge aft of it""" if y <= Y_C0: return HOODth(y) t1 = theta_of_z(Y_C1, SHELLz(Y_C1)) if y < Y_C1: t = (y - Y_C0) / (Y_C1 - Y_C0) return HOODth(Y_C0) + (t1 - HOODth(Y_C0)) * smoothstep(0.0, 1.0, t) return theta_of_z(y, SHELLz(y)) def edge_sep(y): """arc between the edge column and the next one up: the hood's frame width, closing to the shell's 3 mm step""" return FR_W + (0.003 - FR_W) * smoothstep(Y_C0, Yi(362.0), y) # ------------------------------------------------------------------ stations def spacing(y): h = 0.13 dn = y - Y_NOSE h = min(h, 0.035 + 0.095 * smoothstep(0.0, 0.9, dn)) ds = Y_TAIL - y h = min(h, 0.05 + 0.08 * smoothstep(0.0, 0.4, ds)) if Y_JAW - 0.1 <= y <= Y_C1 + 0.1: h = min(h, 0.07) if WIN["ya"] - 0.1 <= y <= WIN["yb"] + 0.1: h = min(h, 0.065) return h Y0 = Yi(X_TIP + 2.5) # first ring, 2 cm behind the point ys = [Y0] while ys[-1] < Y_TAIL - 1e-6: ys.append(min(Y_TAIL, ys[-1] + spacing(ys[-1] + 0.5 * spacing(ys[-1])))) special = [Y_JAW, Y_JAW + FR_W, Y_HOOD, Y_HOOD + FR_W, Y_C0, Y_C1, HY_A, HY_B, Y_BULK, Y_BAY, WIN["ya"] - FR_W, WIN["ya"], WIN["yb"], WIN["yb"] + FR_W] for s in special: ys = [y for y in ys if abs(y - s) > 0.45 * spacing(s)] + [s] for s in (Y_C0 + STEP, Y_C1 + STEP): # the collar's faces: 6 mm after their stations, nothing in between ys = [y for y in ys if not (s - STEP < y < s + 0.02)] + [s] ys = sorted(set(round(y, 6) for y in ys)) if Y_TAIL - ys[-1] > 1e-4: ys.append(Y_TAIL) YS = ys NR = len(YS) def st(y): return min(range(NR), key=lambda i: abs(YS[i] - y)) I_JAW, I_JAWF = st(Y_JAW), st(Y_JAW + FR_W) I_HOOD, I_HOODF = st(Y_HOOD), st(Y_HOOD + FR_W) I_C0, I_C0S, I_C1, I_C1S = st(Y_C0), st(Y_C0 + STEP), st(Y_C1), st(Y_C1 + STEP) I_HA, I_HB = st(HY_A), st(HY_B) I_BULK, I_BAY = st(Y_BULK), st(Y_BAY) WIN["ia"], WIN["ib"] = st(WIN["ya"]), st(WIN["yb"]) WIN["fa"], WIN["fb"] = st(WIN["ya"] - FR_W), st(WIN["yb"] + FR_W) assert I_C0S == I_C0 + 1 and I_C1S == I_C1 + 1, (I_C0, I_C0S, I_C1, I_C1S) assert I_HOOD < I_HA < I_C0 < I_HB < I_C1, (I_HOOD, I_HA, I_C0, I_HB, I_C1) # ------------------------------------------------------------------ columns per station (arc length, pinned at the outlines) def columns(y): th, arc = half_table(y) L = arc[-1] def a_of(t): return interp(th, arc, t) ctrl = {0: 0.0, HALF: L} dflt = lambda j: L * j / HALF def pin(j, tgt, w): ctrl[j] = dflt(j) + (tgt - dflt(j)) * w w = weight(y, Y_JAW, Y_C0, FR_W) # the glass's lower edge and its sill if w > 0: yc = min(max(y, Y_JAW), Y_C0) aB = a_of(theta_of_z(y, CHINz(yc))) pin(J_CH - 1, aB - FR_W, w); pin(J_CH, aB, w) w = weight(y, Y_HOOD, Y_TAIL, FR_W) # the hood's edge, then the shell's if w > 0: yc = max(y, Y_HOOD) aE = a_of(edge_theta(yc)) pin(J_HE, aE, w); pin(J_HE + 1, aE + edge_sep(yc), w) w = weight(y, WIN["ya"], WIN["yb"], FR_W) # the rear bay's window if w > 0: yc = min(max(y, WIN["ya"]), WIN["yb"]) zlo, zhi = side_span(WIN["poly"], yc) aB, aT = a_of(theta_of_z(y, zlo)), a_of(theta_of_z(y, zhi)) for j, tgt in ((J_WB - 1, aB - FR_W), (J_WB, aB), (J_WT, aT), (J_WT + 1, aT + FR_W)): pin(j, tgt, w) w = weight(y, HY_A, HY_B, FR_H) # the hatch under the saddle if w > 0: aE, aF = a_of(theta_of_lat(y, HATCH_R)), a_of(theta_of_lat(y, HATCH_R + FR_H)) pin(J_HH - 1, aF, w); pin(J_HH, aE, w) keys = sorted(ctrl) vals = [ctrl[k] for k in keys] for i in range(1, len(vals)): # keep them in order vals[i] = max(vals[i], vals[i - 1] + 0.002) a_j = [interp(keys, vals, j) for j in range(HALF + 1)] t_j = [interp(arc, th, a) for a in a_j] t_j[0], t_j[HALF] = -math.pi / 2, math.pi / 2 return t_j + [math.pi - t_j[HALF - k] for k in range(1, HALF)] TH = [columns(y) for y in YS] BASE = [[sec(YS[i], t) for t in TH[i]] for i in range(NR)] NRM = [[normal_at(p) for p in row] for row in BASE] def fj(k): """the port face index of ring face k (faces k = 32..63 mirror 31..0)""" k %= M return k if k < HALF else M - 1 - k def vj(k): """the port column of ring vertex k""" k %= M return k if k <= HALF else M - k # ------------------------------------------------------------------ roles of the grid faces (i, k): between stations i, i+1 and columns k, k+1 ROLE = {} for i in range(NR - 1): for k in range(M): f = fj(k) r = "skin" if i < I_JAW or (i < I_HOOD and f >= J_CH) or (i < I_C0 and J_CH <= f < J_HE): r = "glass" elif i < I_C0 and (f == J_CH - 1 or (I_JAW <= i < I_JAWF and f < J_CH)): r = "frame" # the sill under the glass, and the jaw's front edge elif I_HOOD <= i < I_C0 and (f == J_HE or (I_HOOD <= i < I_HOODF and f >= J_HE)): r = "frame" # the hood's edge and its front elif i == I_C0: r = "frame" if J_CH <= f < J_HE else "collar" # the collar's front face: a dark seal where it meets the glass elif I_C0S <= i <= I_C1: r = "collar" elif i < I_C0: r = "hood" if f > J_HE else "jaw" elif f >= J_HE: r = "shell" # face J_HE is the shell's step else: r = "lower" if I_HA <= i < I_HB and f >= J_HH: r = "hatch" if WIN["ia"] <= i < WIN["ib"] and J_WB <= f < J_WT: r = "window" elif WIN["fa"] <= i < WIN["fb"] and ((J_WB <= f < J_WT) or f in (J_WB - 1, J_WT)): r = "frame" ROLE[(i, k)] = r HOLES = ("glass", "hatch", "window") # ------------------------------------------------------------------ vertex positions: raises and the rims round each hole def base_raise(i, j): if I_C0S <= i <= I_C1: return H_COL_LOW + (H_COL - H_COL_LOW) * smoothstep(J_CH - 4, J_CH - 1, j) if i >= I_C1S and j >= J_HE + 1: return H_SHELL return 0.0 def boundary_loops(kind): """closed loops of grid vertices round the faces of one hole kind (each side separately)""" edges = {} for (i, k), r in ROLE.items(): if r != kind: continue q = [(i, k % M), (i, (k + 1) % M), (i + 1, (k + 1) % M), (i + 1, k % M)] for a, b in zip(q, q[1:] + q[:1]): key = (a, b) if a < b else (b, a) edges[key] = edges.get(key, 0) + 1 border = [e for e, n in edges.items() if n == 1] # the hull grid's open front ring (stations < I_JAW are all glass) is not a border with the skin border = [e for e in border if not (e[0][0] == 0 and e[1][0] == 0)] nxt = {} for a, b in border: nxt.setdefault(a, []).append(b) nxt.setdefault(b, []).append(a) loops, seen = [], set() for start in nxt: if start in seen: continue loop, prev, cur = [start], None, start seen.add(start) while True: cand = [v for v in nxt[cur] if v != prev and (v not in seen or (v == start and len(loop) > 2))] if not cand: break v = cand[0] if v == start: break loop.append(v); seen.add(v); prev, cur = cur, v loops.append(loop) return loops LOOPS = {kind: boundary_loops(kind) for kind in HOLES} RIM = {} for kind, rim in (("glass", RIM_W), ("window", RIM_W), ("hatch", RIM_H)): for loop in LOOPS[kind]: for ik in loop: RIM[ik] = rim def P(ik): i, k = ik[0], ik[1] % M return BASE[i][k] + NRM[i][k] * (base_raise(i, vj(k)) + RIM.get((i, k), 0.0)) # seam field: transverse panel seams on the shell and the lower hull (5 + signed distance; 0 where a vertex has none) SEAM_YS = [Yi(x) for x in SEAMS_X[1:]] def seam_ring(p): if p.y < Y_C1 + 0.05: return 0.0 d = min((p.y - sy for sy in SEAM_YS), key=abs) return 5.0 + d if abs(d) < 0.30 else 0.0 # ------------------------------------------------------------------ the hull mesh: skin, frames, collar, sleeves, stern PAINT_ROLES = ("collar", "hood", "jaw", "shell") hull = MB("SM_Mako_Hull", [PAINT, DARK, GRAPH]) MI_P, MI_D, MI_G = hull.mi(PAINT), hull.mi(DARK), hull.mi(GRAPH) bm = bmesh.new() lay_r = bm.verts.layers.float.new("seam_ring") lay_n = [bm.verts.layers.float.new(n) for n in ("an_x", "an_y", "an_z")] lay_s = bm.faces.layers.int.new("on_skin") V = {} def hv(ik): i, k = ik[0], ik[1] % M if (i, k) not in V: p = P((i, k)) v = bm.verts.new(p) v[lay_r] = seam_ring(p) for c, l in zip(NRM[i][k], lay_n): v[l] = c V[(i, k)] = v return V[(i, k)] for (i, k), role in ROLE.items(): if role in HOLES: continue f = bm.faces.new([hv(a) for a in ((i, k), (i, (k + 1) % M), (i + 1, (k + 1) % M), (i + 1, k))]) f.material_index = MI_G if role == "frame" else (MI_P if role in PAINT_ROLES else MI_D) # smooth custom normals only on faces of one level (not the collar's faces, the shell's step, frames) fjk = fj(k) flat_step = (i in (I_C0, I_C1)) or (i >= I_C1S and fjk == J_HE) f[lay_s] = 0 if (role == "frame" or flat_step or any(a in RIM for a in ((i, k % M), (i, (k + 1) % M), (i + 1, (k + 1) % M), (i + 1, k % M)))) else 1 # sleeves: from each hole's raised edge down to the lining (through the glass's edge) SLEEVES = {} for kind in HOLES: for loop in LOOPS[kind]: rows = [[hv(ik) for ik in loop]] for d in (GLASS_D, LINER): rows.append([bm.verts.new(BASE[i][k % M] - NRM[i][k % M] * d) for i, k in loop]) ring_rows(bm, rows, closed=True, mat_index=MI_G) SLEEVES.setdefault(kind, []).append(loop) # stern: the last ring steps in and down into a recessed panel, filled by a quad grid (the stern housing covers it) iE = NR - 1 cen = Vector((0, YS[iE], ZMf(YS[iE]))) rE = [hv((iE, k)) for k in range(M)] rows = [rE] for sc_, dy in ((0.94, 0.0), (0.90, -0.03), (0.80, -0.03)): rows.append([bm.verts.new(cen + (v.co - cen) * sc_ + Vector((0, dy, 0))) for v in rE]) ring_rows(bm, rows[:3], closed=True, mat_index=MI_D) ring_rows(bm, rows[2:], closed=True, mat_index=MI_G) coons_cap(bm, rows[-1][::-1], MI_G) bm.normal_update() bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) top_f = max(bm.faces, key=lambda f: f.calc_center_median().z) if top_f.normal.z < 0: bmesh.ops.reverse_faces(bm, faces=bm.faces[:]) bm.normal_update() hull.merge(bm, None) REPORT["hull_stations"] = NR REPORT["hull_columns"] = M REPORT["boundary_loops"] = {k: [len(l) for l in v] for k, v in LOOPS.items()} # ------------------------------------------------------------------ glass: the canopy's and the windows' own grid, sunk to the sleeve def glass_sink(i): """the nose is all glass: no sink at its point, the full 22 mm from 0.3 m back""" return GLASS_D * smoothstep(0.0, 0.30, YS[i] - Y_NOSE) glass = MB("SM_Mako_Glass", [GLASS]) bmg = bmesh.new() G = {} def gv(i, k): k %= M if (i, k) not in G: G[(i, k)] = bmg.verts.new(BASE[i][k] - NRM[i][k] * glass_sink(i)) return G[(i, k)] for (i, k), role in ROLE.items(): if role in ("glass", "window"): bmg.faces.new((gv(i, k), gv(i, k + 1), gv(i + 1, k + 1), gv(i + 1, k))) tipg = bmg.verts.new((0, Y_NOSE, TOPf(Y_NOSE))) for k in range(M): # the nose point: a fan (a cone's tip) bmg.faces.new((gv(0, k + 1), gv(0, k), tipg)) bmg.normal_update() for f in bmg.faces: c = f.calc_center_median() out_dir = Vector((c.x, 0, c.z - ZMf(min(max(c.y, Y_NOSE + 0.01), Y_TAIL)))) if c.y < Y_NOSE + 0.05: out_dir = Vector((0, -1, 0)) if f.normal.dot(out_dir) < 0: f.normal_flip() glass.merge(bmg, GLASS) # ------------------------------------------------------------------ cabin lining: the grid sunk 60 mm, from the jaw's front to the rear bay's end cab = MB("SM_Mako_Cabin", [CAB, FLOOR, SEAT, SCREEN, CABGLOW, GRAPH, STEEL]) bml = bmesh.new() LV = {} def lv(i, k): k %= M if (i, k) not in LV: LV[(i, k)] = bml.verts.new(BASE[i][k] - NRM[i][k] * LINER) return LV[(i, k)] for i in range(I_JAW, I_BAY): for k in range(M): if ROLE[(i, k)] in HOLES: continue bml.faces.new((lv(i, k), lv(i + 1, k), lv(i + 1, (k + 1) % M), lv(i, (k + 1) % M))) coons_cap(bml, [lv(I_BAY, k) for k in range(M)]) bml.normal_update() for f in bml.faces: # seen from inside: toward the axis line c = f.calc_center_median() ax = Vector((0, c.y, ZMf(min(max(c.y, Y_NOSE), Y_TAIL)))) if abs(f.normal.y) < 0.9 and f.normal.dot(ax - c) < 0: f.normal_flip() elif abs(f.normal.y) >= 0.9 and f.normal.y > 0: f.normal_flip() cab.merge(bml, CAB) # the cockpit's rear bulkhead: the lining's section filled, both faces (the cockpit and the rear bay see it) for d, sgn in ((0.0, -1), (0.03, 1)): bmb = bmesh.new() ring = [bmb.verts.new(BASE[I_BULK][k] - NRM[I_BULK][k] * LINER + Vector((0, d, 0))) for k in range(M)] coons_cap(bmb, ring) bmb.normal_update() if sum(f.normal.y for f in bmb.faces) * sgn < 0: bmesh.ops.reverse_faces(bmb, faces=bmb.faces[:]) cab.merge(bmb, CAB) REPORT["stage"] = "hull+glass+lining"