""" DX-07 deep-ore mining submarine - procedural generator for Blender 5.x Style: weathered retro-futuristic industrial hard-surface (cassette-futurism / used-future). Axes: +X forward (bow/drill), +Y port, +Z up. Units: metres. Usage inside Blender (Scripting tab or MCP): exec(open(r"/dx07_generator.py").read()); build(); setup_render(); Everything lives in the "DX07_Submarine" collection and is rebuilt from scratch on each build(). """ import bpy, bmesh, math, random, os from mathutils import Vector, Matrix, Quaternion COLL = "DX07_Submarine" G = {} TAU = math.tau FONT_PATH = "C:/Windows/Fonts/bahnschrift.ttf" # =============================================================== basic math def lerp(a, b, t): return a + (b - a) * t def catmull(p0, p1, p2, p3, t): t2 = t * t t3 = t2 * t return 0.5 * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3) def resample_path(ctrl, step): """Catmull-Rom through ctrl points, resampled at ~even arc length.""" pts = [Vector(p) for p in ctrl] ext = [pts[0] + (pts[0] - pts[1])] + pts + [pts[-1] + (pts[-1] - pts[-2])] dense = [] for i in range(1, len(ext) - 2): for k in range(40): dense.append(catmull(ext[i - 1], ext[i], ext[i + 1], ext[i + 2], k / 40)) dense.append(pts[-1]) cum = [0.0] for a, b in zip(dense[:-1], dense[1:]): cum.append(cum[-1] + (b - a).length) total = cum[-1] n = max(2, int(total / step) + 1) out, j = [], 0 for i in range(n): s = total * i / (n - 1) while j < len(cum) - 2 and cum[j + 1] < s: j += 1 seg = cum[j + 1] - cum[j] t = 0 if seg == 0 else (s - cum[j]) / seg out.append((dense[j].lerp(dense[j + 1], t), s)) return out, total def basis(z, x_hint): z = Vector(z).normalized() x = Vector(x_hint) x = (x - z * x.dot(z)) if x.length < 1e-6: x = Vector((1, 0, 0)) if abs(z.x) < 0.9 else Vector((0, 1, 0)) x = (x - z * x.dot(z)) x.normalize() y = z.cross(x) return x, y, z def frame(loc, z, x_hint=(1, 0, 0)): x, y, zz = basis(z, x_hint) m = Matrix((x, y, zz)).transposed().to_4x4() m.translation = Vector(loc) return m # =============================================================== hull definition # key sections: (x, half-width, top height, bottom height, superellipse exponent, z-centre) HOLD = [(-2.78, 0.93, 0.92, 0.92, 2.2, 0.00), (-2.62, 1.06, 1.00, 1.03, 2.2, 0.00), (-2.20, 1.13, 1.05, 1.10, 2.25, -0.02), (-1.30, 1.18, 1.08, 1.15, 2.3, -0.03), (-0.30, 1.20, 1.10, 1.18, 2.3, -0.04), (0.70, 1.17, 1.08, 1.14, 2.3, -0.03), (1.50, 1.10, 1.03, 1.06, 2.25, -0.01), (1.86, 1.02, 0.98, 0.99, 2.2, 0.00), (2.02, 0.93, 0.92, 0.92, 2.2, 0.00)] FWD = [(1.98, 0.93, 0.92, 0.92, 2.2, 0.0), (2.16, 1.03, 0.97, 0.99, 2.2, 0.0), (2.50, 1.08, 1.00, 1.02, 2.2, 0.0), (3.00, 1.02, 0.95, 0.96, 2.15, 0.0), (3.45, 0.88, 0.84, 0.84, 2.1, 0.0), (3.75, 0.76, 0.73, 0.73, 2.05, 0.0), (3.92, 0.70, 0.68, 0.68, 2.0, 0.0)] STERN = [(-4.68, 0.22, 0.21, 0.21, 2.0, 0.0), (-4.55, 0.46, 0.44, 0.44, 2.05, 0.0), (-4.30, 0.70, 0.66, 0.66, 2.1, 0.0), (-3.90, 0.90, 0.84, 0.84, 2.15, 0.0), (-3.40, 1.02, 0.95, 0.97, 2.2, 0.0), (-3.00, 1.03, 0.97, 1.00, 2.2, 0.0), (-2.82, 0.93, 0.92, 0.92, 2.2, 0.0)] def eval_keys(keys, x): keys = sorted(keys, key=lambda k: k[0]) if x <= keys[0][0]: return keys[0][1:] if x >= keys[-1][0]: return keys[-1][1:] for i in range(len(keys) - 1): if keys[i][0] <= x <= keys[i + 1][0]: break t = (x - keys[i][0]) / (keys[i + 1][0] - keys[i][0]) k0 = keys[max(i - 1, 0)] k1, k2 = keys[i], keys[i + 1] k3 = keys[min(i + 2, len(keys) - 1)] return tuple(catmull(k0[j], k1[j], k2[j], k3[j], t) for j in range(1, 6)) def se_ring(w, ht, hb, n, cz, seg, t0=0.0): pts = [] for i in range(seg): t = t0 + TAU * i / seg c, s = math.cos(t), math.sin(t) y = w * math.copysign(abs(c) ** (2 / n), c) zz = math.copysign(abs(s) ** (2 / n), s) z = cz + (ht if zz > 0 else hb) * zz pts.append((y, z)) return pts def se_point(p, t): w, ht, hb, n, cz = p c, s = math.cos(t), math.sin(t) y = w * math.copysign(abs(c) ** (2 / n), c) zz = math.copysign(abs(s) ** (2 / n), s) return y, cz + (ht if zz > 0 else hb) * zz def surf(keys, x, t, grow=0.0): """Surface point + outward normal on a lofted module. t=0 port(+Y), pi/2 top, pi starboard, -pi/2 belly.""" def P(xx, tt): w, ht, hb, n, cz = eval_keys(keys, xx) y, z = se_point((w + grow, ht + grow, hb + grow, n, cz), tt) return Vector((xx, y, z)) p = P(x, t) dx = P(x + 0.01, t) - P(x - 0.01, t) dt = P(x, t + 0.004) - P(x, t - 0.004) nrm = dx.cross(dt).normalized() radial = Vector((0, p.y, p.z - eval_keys(keys, x)[4])) if nrm.dot(radial) < 0: nrm = -nrm return p, nrm def inside_hull(pt): x = pt.x for keys in (HOLD, FWD, STERN): xs = [k[0] for k in keys] if min(xs) <= x <= max(xs): w, ht, hb, n, cz = eval_keys(keys, x) h = ht if pt.z - cz > 0 else hb if (abs(pt.y) / w) ** n + (abs(pt.z - cz) / h) ** n < 1.0: return True return False # =============================================================== builder class B: """Accumulates geometry into one bmesh with per-part material index.""" def __init__(self): self.bm = bmesh.new() def begin(self): self.bm.verts.ensure_lookup_table() self.bm.faces.ensure_lookup_table() self.v0 = len(self.bm.verts) self.f0 = len(self.bm.faces) def end(self, mat=0, m=None): bm = self.bm bm.verts.ensure_lookup_table() bm.faces.ensure_lookup_table() vs = bm.verts[self.v0:] if m is not None: bmesh.ops.transform(bm, matrix=m, verts=vs) for f in bm.faces[self.f0:]: f.material_index = mat return vs # ---- primitives (all built in local space, placed with matrix m) def cyl(self, r, depth, seg=24, mat=0, m=None, r2=None): self.begin() bmesh.ops.create_cone(self.bm, cap_ends=True, cap_tris=False, segments=seg, radius1=r, radius2=r if r2 is None else r2, depth=depth) return self.end(mat, m) def box(self, sx, sy, sz, mat=0, m=None): self.begin() bmesh.ops.create_cube(self.bm, size=1.0) self.bm.verts.ensure_lookup_table() for v in self.bm.verts[self.v0:]: v.co = Vector((v.co.x * sx, v.co.y * sy, v.co.z * sz)) return self.end(mat, m) def sphere(self, r, u=24, v=16, mat=0, m=None): self.begin() bmesh.ops.create_uvsphere(self.bm, u_segments=u, v_segments=v, radius=r) return self.end(mat, m) def lathe(self, prof, seg=32, mat=0, m=None, closed=True, arc=None): """Revolve (r, h) profile around local Z.""" self.begin() bm = self.bm full = arc is None steps = seg if full else seg + 1 rings = [] for i in range(steps): a = (TAU * i / seg) if full else (arc[0] + (arc[1] - arc[0]) * i / seg) c, s = math.cos(a), math.sin(a) rings.append([bm.verts.new((r * c, r * s, h)) for (r, h) in prof]) n = len(prof) for i in range(seg): A = rings[i] Bn = rings[(i + 1) % steps] if full else rings[i + 1] for k in (range(n) if closed else range(n - 1)): k2 = (k + 1) % n try: bm.faces.new((A[k], Bn[k], Bn[k2], A[k2])) except ValueError: pass if not full and closed: for ring in (rings[0], rings[-1]): try: bm.faces.new(ring) except ValueError: pass return self.end(mat, m) def loft(self, sections, seg=64, mat=0, cap0='ngon', cap1='ngon', t0=0.0): """sections: list of (x, w, ht, hb, n, cz) (or with extra cy).""" self.begin() bm = self.bm rings = [] for sec in sections: x, w, ht, hb, n, cz = sec[:6] cy = sec[6] if len(sec) > 6 else 0.0 rings.append([bm.verts.new((x, cy + y, z)) for (y, z) in se_ring(w, ht, hb, n, cz, seg, t0)]) for a, b in zip(rings[:-1], rings[1:]): for i in range(seg): j = (i + 1) % seg bm.faces.new((a[i], a[j], b[j], b[i])) for ring, cap in ((rings[0], cap0), (rings[-1], cap1)): if cap == 'ngon': bm.faces.new(ring) elif isinstance(cap, tuple) and cap[0] == 'pole': pv = bm.verts.new(cap[1]) for i in range(seg): bm.faces.new((ring[i], ring[(i + 1) % seg], pv)) return self.end(mat, None) def sweep(self, ctrl, radius, seg=16, step=0.03, mat=0, caps=True, radius_fn=None): path, total = resample_path(ctrl, step) self.begin() bm = self.bm P = [p for p, s in path] T = [] for i in range(len(P)): a = P[max(i - 1, 0)] b = P[min(i + 1, len(P) - 1)] T.append((b - a).normalized()) up = Vector((0, 0, 1)) if abs(T[0].z) < 0.9 else Vector((1, 0, 0)) Nn = (up - T[0] * up.dot(T[0])).normalized() rings = [] for i, (p, s) in enumerate(path): if i > 0: q = T[i - 1].rotation_difference(T[i]) Nn = q @ Nn Nn = (Nn - T[i] * Nn.dot(T[i])).normalized() Bv = T[i].cross(Nn) r = radius_fn(s, total) if radius_fn else radius rings.append([bm.verts.new(p + (Nn * math.cos(TAU * k / seg) + Bv * math.sin(TAU * k / seg)) * r) for k in range(seg)]) for a, b in zip(rings[:-1], rings[1:]): for k in range(seg): k2 = (k + 1) % seg bm.faces.new((a[k], a[k2], b[k2], b[k])) if caps: bm.faces.new(rings[0]) bm.faces.new(rings[-1]) self.end(mat, None) return path, T, total def hexbolt(self, r=0.02, h=0.025, mat=0, m=None): self.cyl(r, h, seg=6, mat=mat, m=m @ Matrix.Translation((0, 0, h / 2)) if m else None) def finish(self, name, mats, col, sharp=35, smooth=True, origin=None, subsurf=0): bm = self.bm bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) col.objects.link(ob) for mt in mats: me.materials.append(mt) if smooth: me.shade_smooth() if sharp: me.set_sharp_from_angle(angle=math.radians(sharp)) else: me.shade_flat() if origin is not None: o = Vector(origin) me.transform(Matrix.Translation(-o)) ob.location = o if subsurf: md = ob.modifiers.new("Subdiv", 'SUBSURF') md.levels = subsurf md.render_levels = subsurf ob.parent = G['root'] return ob # =============================================================== materials def _n(nt, typ, x=0, y=0, **kw): n = nt.nodes.new(typ) n.location = (x, y) for k, v in kw.items(): setattr(n, k, v) return n def _in(node, name, typ=None): for s in node.inputs: if s.name == name and s.enabled and (typ is None or s.type == typ): return s raise KeyError(name) def _out(node, name, typ=None): for s in node.outputs: if s.name == name and s.enabled and (typ is None or s.type == typ): return s raise KeyError(name) class MB: """Tiny node-building helper for weathered materials.""" def __init__(self, name): m = bpy.data.materials.get(name) if m: bpy.data.materials.remove(m) self.m = bpy.data.materials.new(name) self.m.use_nodes = True self.nt = self.m.node_tree self.nt.nodes.clear() self.x = -1600 self.out = _n(self.nt, 'ShaderNodeOutputMaterial', 600, 0) self.bsdf = _n(self.nt, 'ShaderNodeBsdfPrincipled', 300, 0) self.link(self.bsdf.outputs['BSDF'], self.out.inputs['Surface']) tc = _n(self.nt, 'ShaderNodeTexCoord', -2200, 0) tc.object = G['root'] self.co = tc.outputs['Object'] self.local = tc.outputs['Object'] self.tc = tc self.geo = _n(self.nt, 'ShaderNodeNewGeometry', -2200, -400) sep = _n(self.nt, 'ShaderNodeSeparateXYZ', -2000, -400) self.link(self.geo.outputs['Normal'], sep.inputs[0]) self.nz = sep.outputs['Z'] sepc = _n(self.nt, 'ShaderNodeSeparateXYZ', -2000, 200) self.link(self.co, sepc.inputs[0]) self.cx = sepc.outputs['X'] self._y = -600 def link(self, a, b): self.nt.links.new(a, b) def node(self, typ, **kw): self._y -= 60 return _n(self.nt, typ, self.x + (len(self.nt.nodes) % 6) * 40, self._y, **kw) def val(self, v): n = self.node('ShaderNodeValue') n.outputs[0].default_value = v return n.outputs[0] def rgb(self, c): n = self.node('ShaderNodeRGB') n.outputs[0].default_value = (*c, 1) return n.outputs[0] def _s(self, sock, v): if isinstance(v, (int, float)) or (isinstance(v, tuple)): if isinstance(v, tuple) and len(v) == 3 and sock.type == 'RGBA': v = (*v, 1) sock.default_value = v else: self.link(v, sock) def math(self, op, a, b=0.0, clamp=False): n = self.node('ShaderNodeMath', operation=op, use_clamp=clamp) self._s(n.inputs[0], a) self._s(n.inputs[1], b) return n.outputs[0] def mr(self, v, a, b, c=0.0, d=1.0, clamp=True): n = self.node('ShaderNodeMapRange', clamp=clamp) self._s(n.inputs['Value'], v) n.inputs['From Min'].default_value = a n.inputs['From Max'].default_value = b n.inputs['To Min'].default_value = c n.inputs['To Max'].default_value = d return n.outputs['Result'] def mapping(self, scale=(1, 1, 1), vec=None): n = self.node('ShaderNodeMapping') self.link(vec if vec is not None else self.co, n.inputs['Vector']) n.inputs['Scale'].default_value = scale return n.outputs['Vector'] def noise(self, scale, detail=4.0, rough=0.55, vec=None, dist=0.0): n = self.node('ShaderNodeTexNoise') self.link(vec if vec is not None else self.co, n.inputs['Vector']) n.inputs['Scale'].default_value = scale n.inputs['Detail'].default_value = detail n.inputs['Roughness'].default_value = rough n.inputs['Distortion'].default_value = dist return n.outputs['Fac'] def voronoi(self, scale, vec=None, rand=1.0): n = self.node('ShaderNodeTexVoronoi') self.link(vec if vec is not None else self.co, n.inputs['Vector']) n.inputs['Scale'].default_value = scale n.inputs['Randomness'].default_value = rand return n.outputs['Distance'] def mix(self, fac, a, b): n = self.node('ShaderNodeMix', data_type='RGBA', clamp_factor=True) self._s(_in(n, 'Factor', 'VALUE'), fac) self._s(_in(n, 'A', 'RGBA'), a) self._s(_in(n, 'B', 'RGBA'), b) return _out(n, 'Result', 'RGBA') def edge(self, radius=0.012, lo=0.02, hi=0.25): bev = self.node('ShaderNodeBevel', samples=8) bev.inputs['Radius'].default_value = radius dot = self.node('ShaderNodeVectorMath', operation='DOT_PRODUCT') self.link(bev.outputs['Normal'], dot.inputs[0]) self.link(self.geo.outputs['Normal'], dot.inputs[1]) inv = self.math('SUBTRACT', 1.0, _out(dot, 'Value')) return self.mr(inv, lo, hi) def cavity(self, dist=0.2): ao = self.node('ShaderNodeAmbientOcclusion', samples=12) ao.inputs['Distance'].default_value = dist return self.math('SUBTRACT', 1.0, ao.outputs['AO'], clamp=True) def bump(self, h, strength=0.3, distance=0.02): b = self.node('ShaderNodeBump') b.inputs['Strength'].default_value = strength b.inputs['Distance'].default_value = distance self._s(b.inputs['Height'], h) self.link(b.outputs['Normal'], self.bsdf.inputs['Normal']) def set(self, name, v): self._s(self.bsdf.inputs[name], v) def weathered_paint(name, base, faded, chip_col, chip_metal, pit_scale=45, pit_amt=1.0, rough=0.6, chips=0.55, silt=0.7, red_under=None): mb = MB(name) # sun-fade / chalky on upward faces up = mb.mr(mb.nz, 0.1, 0.95) fade_n = mb.noise(1.6, 3, 0.5) fade = mb.math('MULTIPLY', up, mb.mr(fade_n, 0.35, 0.7, 0.15, 0.75)) col = mb.mix(fade, base, faded) # lengthwise flow streaks (water runs aft) streak = mb.noise(7.0, 6, 0.6, vec=mb.mapping((0.18, 6.0, 6.0))) streak = mb.mr(streak, 0.48, 0.68, 0.0, 0.45) col = mb.mix(streak, col, tuple(c * 0.45 for c in base)) # cavity grime cav = mb.cavity(0.18) col = mb.mix(mb.math('MULTIPLY', cav, 0.75), col, (0.035, 0.028, 0.022)) # edge chips (pale bare metal / primer) e = mb.edge(0.015, 0.015, 0.18) chn = mb.noise(22.0, 8, 0.7) chip_mask = mb.math('MULTIPLY', e, mb.mr(chn, 0.38, 0.6, 0.0, 1.6)) scr = mb.noise(3.5, 6, 0.6) # broad worn patches chip_mask = mb.math('ADD', chip_mask, mb.mr(scr, 0.66, 0.72, 0.0, chips * 0.6)) chip_mask = mb.mr(chip_mask, 0.45, 0.55) if red_under: under = mb.mr(mb.noise(9.0, 4, 0.6), 0.45, 0.55) chip_c = mb.mix(under, red_under, chip_col) else: chip_c = chip_col col = mb.mix(chip_mask, col, chip_c) # impact pits: clustered, densest toward the bow xg = mb.mr(mb.cx, -4.7, 5.5, 0.15, 1.0) clus = mb.mr(mb.noise(2.2, 3, 0.5), 0.5, 0.64) v1 = mb.voronoi(pit_scale) pit_small = mb.mr(v1, 0.14, 0.09) v2 = mb.voronoi(pit_scale * 0.28) pit_big = mb.mr(v2, 0.075, 0.045) pits = mb.math('MAXIMUM', pit_small, pit_big) pits = mb.math('MULTIPLY', pits, mb.math('MULTIPLY', clus, xg)) pits = mb.math('MULTIPLY', pits, pit_amt, clamp=True) col = mb.mix(pits, col, (0.03, 0.02, 0.015)) # silt on upward faces siltm = mb.math('MULTIPLY', mb.mr(mb.nz, 0.55, 0.95), mb.mr(mb.noise(5.0, 5, 0.6), 0.42, 0.62, 0.0, silt)) col = mb.mix(siltm, col, (0.30, 0.27, 0.23)) # belly scrapes (lengthwise) down = mb.mr(mb.nz, -0.45, -0.9) scrape = mb.noise(30.0, 2, 0.4, vec=mb.mapping((0.08, 5.0, 5.0))) scrape = mb.math('MULTIPLY', down, mb.mr(scrape, 0.6, 0.66)) col = mb.mix(scrape, col, (0.42, 0.40, 0.38)) mb.set('Base Color', col) # roughness / metal r = mb.mr(chip_mask, 0.0, 1.0, rough, 0.38) r = mb.math('ADD', r, mb.math('MULTIPLY', siltm, 0.35)) r = mb.math('ADD', r, mb.mr(mb.noise(12.0, 4, 0.5), 0.3, 0.7, -0.08, 0.08)) mb.set('Roughness', mb.math('MAXIMUM', mb.math('MAXIMUM', r, mb.math('MULTIPLY', pits, 0.85)), 0.2)) mb.set('Metallic', mb.math('MULTIPLY', chip_mask, chip_metal)) h = mb.math('SUBTRACT', mb.math('MULTIPLY', mb.noise(60.0, 6, 0.6), 0.25), mb.math('ADD', pits, mb.math('MULTIPLY', chip_mask, 0.35))) mb.bump(h, 0.35, 0.012) return mb.m def simple_metal(name, base, rough=0.4, metal=1.0, grime=0.6, polish=0.0, mud=0.0, scrape=0.0): mb = MB(name) col = base nz = mb.mr(mb.noise(14.0, 6, 0.6), 0.3, 0.7, 0.85, 1.15) cav = mb.cavity(0.12) col = mb.mix(mb.math('MULTIPLY', cav, grime), col, (0.03, 0.025, 0.02)) e = mb.edge(0.01, 0.02, 0.2) if polish > 0 else None if e is not None: col = mb.mix(mb.math('MULTIPLY', e, polish), col, (0.78, 0.77, 0.75)) mudm = None if mud > 0: mn = mb.noise(6.0, 6, 0.65, dist=0.4) mudm = mb.math('ADD', mb.math('MULTIPLY', cav, 1.3), mb.mr(mn, 0.45, 0.7, -0.3, 0.6)) mudm = mb.math('MULTIPLY', mb.mr(mudm, 0.35, 0.6), mud) col = mb.mix(mudm, col, (0.16, 0.13, 0.095)) if scrape > 0: sc = mb.noise(35.0, 2, 0.4, vec=mb.mapping((0.07, 5.0, 5.0))) scm = mb.math('MULTIPLY', mb.mr(sc, 0.6, 0.67), scrape) col = mb.mix(scm, col, (0.62, 0.61, 0.6)) mb.set('Base Color', col) r = mb.math('MULTIPLY', nz, rough) if e is not None: r = mb.mix(mb.math('MULTIPLY', e, polish), (rough,) * 3, (0.18,) * 3) r = mb.math('MULTIPLY', r, 1.0) if mudm is not None: r = mb.math('ADD', r, mb.math('MULTIPLY', mudm, 0.6)) mb.set('Metallic', mb.math('SUBTRACT', metal, mb.math('MULTIPLY', mudm, metal))) else: mb.set('Metallic', metal) mb.set('Roughness', r) mb.bump(mb.math('ADD', mb.noise(80.0, 6, 0.6), mb.math('MULTIPLY', mudm, 1.5) if mudm is not None else 0.0), 0.2, 0.01) return mb.m def hazard_mat(name, stripes=10, slant=5.0): mb = MB(name) tc = mb.tc sep = mb.node('ShaderNodeSeparateXYZ') mb.link(tc.outputs['Object'], sep.inputs[0]) tc.object = None # object's own local coordinates ang = mb.math('ARCTAN2', sep.outputs['Z'], sep.outputs['Y']) u = mb.math('ADD', mb.math('MULTIPLY', ang, stripes / TAU), mb.math('MULTIPLY', sep.outputs['X'], slant)) f = mb.math('FRACT', u) band = mb.math('GREATER_THAN', f, 0.5) col = mb.mix(band, (0.72, 0.5, 0.03), (0.025, 0.025, 0.025)) e = mb.edge(0.01, 0.02, 0.2) chip = mb.math('MULTIPLY', e, mb.mr(mb.noise(20.0, 8, 0.7), 0.4, 0.6, 0, 1.5)) chip = mb.mr(chip, 0.45, 0.55) col = mb.mix(chip, col, (0.45, 0.44, 0.42)) cav = mb.cavity(0.12) col = mb.mix(mb.math('MULTIPLY', cav, 0.8), col, (0.12, 0.09, 0.06)) mb.set('Base Color', col) mb.set('Roughness', mb.mr(chip, 0, 1, 0.55, 0.3)) mb.set('Metallic', mb.math('MULTIPLY', chip, 0.8)) return mb.m def stencil_mat(name, color, wear=0.5, rough=0.7): mb = MB(name) mb.set('Base Color', color) mb.set('Roughness', rough) n1 = mb.noise(16.0, 8, 0.7) n2 = mb.noise(3.0, 3, 0.5) a = mb.math('ADD', mb.mr(n1, 0.35 + wear * 0.12, 0.45 + wear * 0.12), mb.mr(n2, 0.62, 0.75, 0.0, -1.0)) mb.set('Alpha', mb.math('MULTIPLY', a, 1.0, clamp=True)) mb.m.surface_render_method = 'DITHERED' return mb.m def flat_mat(name, color, rough=0.5, metal=0.0, emit=None, strength=0.0, transmission=0.0, ior=1.45): mb = MB(name) mb.set('Base Color', color) mb.set('Roughness', rough) mb.set('Metallic', metal) if emit: mb.set('Emission Color', emit) mb.set('Emission Strength', strength) if transmission: mb.set('Transmission Weight', transmission) mb.set('IOR', ior) return mb.m def build_materials(): M = {} M['red'] = weathered_paint('DX07_RedPaint', (0.30, 0.022, 0.018), (0.42, 0.13, 0.11), (0.50, 0.46, 0.43), 0.55, pit_scale=42, pit_amt=1.0, rough=0.62) M['char'] = weathered_paint('DX07_CharcoalCoat', (0.036, 0.038, 0.042), (0.075, 0.075, 0.078), (0.20, 0.20, 0.205), 0.2, pit_scale=40, pit_amt=0.55, rough=0.42, chips=0.45, silt=0.6, red_under=(0.28, 0.03, 0.025)) M['rubber'] = weathered_paint('DX07_Rubber', (0.018, 0.018, 0.02), (0.04, 0.04, 0.04), (0.07, 0.07, 0.07), 0.0, pit_scale=30, pit_amt=0.2, rough=0.78, chips=0.1, silt=0.8) M['steel'] = simple_metal('DX07_SteelDark', (0.16, 0.16, 0.17), rough=0.45, polish=0.8, grime=0.7) M['skid'] = simple_metal('DX07_SkidSteel', (0.14, 0.14, 0.15), rough=0.5, polish=0.6, grime=0.7, scrape=1.0, mud=0.5) M['cutter'] = simple_metal('DX07_CutterSteel', (0.34, 0.34, 0.35), rough=0.35, polish=1.0, grime=0.5, mud=1.0) M['bit'] = simple_metal('DX07_Carbide', (0.28, 0.28, 0.3), rough=0.25, polish=1.0, grime=0.3) M['hazard'] = hazard_mat('DX07_Hazard') M['glass'] = flat_mat('DX07_Glass', (0.55, 0.72, 0.68), rough=0.04, transmission=1.0, ior=1.52) M['cavity'] = flat_mat('DX07_CockpitInterior', (0.01, 0.01, 0.01), rough=0.8, emit=(1.0, 0.42, 0.12), strength=0.35) M['void'] = flat_mat('DX07_Void', (0.004, 0.004, 0.004), rough=1.0) M['lamp'] = flat_mat('DX07_LampLens', (0.9, 0.9, 0.85), rough=0.1, emit=(1.0, 0.9, 0.75), strength=30.0) M['strobe'] = flat_mat('DX07_Strobe', (0.9, 0.4, 0.1), rough=0.2, emit=(1.0, 0.35, 0.05), strength=15.0) M['st_white'] = stencil_mat('DX07_StencilWhite', (0.74, 0.72, 0.64), wear=0.8) M['st_yellow'] = stencil_mat('DX07_StencilYellow', (0.72, 0.52, 0.04), wear=0.4) M['st_black'] = stencil_mat('DX07_StencilBlack', (0.02, 0.02, 0.02), wear=0.2) M['grit'] = flat_mat('DX07_WalkwayGrit', (0.045, 0.045, 0.047), rough=0.95) M['seam'] = flat_mat('DX07_Seam', (0.015, 0.012, 0.01), rough=0.9) # grit bump nt = M['grit'].node_tree return M # =============================================================== parts def build_hull(M, C): # --- ore hold (red) b = B() xs = [] x0, x1 = -2.78, 2.02 n = int((x1 - x0) / 0.05) secs = [(x0 + (x1 - x0) * i / n,) + eval_keys(HOLD, x0 + (x1 - x0) * i / n) for i in range(n + 1)] b.loft(secs, seg=72, mat=0) G['hold'] = b.finish("DX07_Hull_OreHold", [M['red']], C['Hull'], sharp=None) # --- forward (drive) module (charcoal) b = B() x0, x1 = 1.98, 3.92 n = int((x1 - x0) / 0.05) secs = [(x0 + (x1 - x0) * i / n,) + eval_keys(FWD, x0 + (x1 - x0) * i / n) for i in range(n + 1)] b.loft(secs, seg=72, mat=0) G['fwd'] = b.finish("DX07_Hull_DriveModule", [M['char']], C['Hull'], sharp=None) # --- stern module (charcoal) b = B() x0, x1 = -4.68, -2.82 n = int((x1 - x0) / 0.04) secs = [(x0 + (x1 - x0) * i / n,) + eval_keys(STERN, x0 + (x1 - x0) * i / n) for i in range(n + 1)] b.loft(secs, seg=72, mat=0, cap0=('pole', (-4.715, 0, 0))) G['stern'] = b.finish("DX07_Hull_SternModule", [M['char']], C['Hull'], sharp=None) # --- joint gasket collars + clamp rings + bolts b = B() for xj in (-2.80, 2.0): base = 0.93 prof = [(-0.14, -0.01), (-0.12, 0.04), (-0.095, 0.075), (-0.06, 0.085), (-0.045, 0.07), (0.045, 0.07), (0.06, 0.085), (0.095, 0.075), (0.12, 0.04), (0.14, -0.01)] secs = [(xj + dx, base + g, base + g, base + g, 2.2, 0.0) for dx, g in prof] b.loft(secs, seg=72, mat=0, cap0=None, cap1=None) for sgn in (-1, 1): # steel clamp rings xr = xj + sgn * 0.078 secs = [(xr - 0.018, base + 0.07, base + 0.07, base + 0.07, 2.2, 0.0), (xr - 0.012, base + 0.098, base + 0.098, base + 0.098, 2.2, 0.0), (xr + 0.012, base + 0.098, base + 0.098, base + 0.098, 2.2, 0.0), (xr + 0.018, base + 0.07, base + 0.07, base + 0.07, 2.2, 0.0)] b.loft(secs, seg=72, mat=1, cap0=None, cap1=None) for i in range(16): t = TAU * (i + 0.5 * (sgn > 0)) / 16 y, z = se_point((base + 0.098, base + 0.098, base + 0.098, 2.2, 0.0), t) nrm = Vector((0, y, z)).normalized() b.hexbolt(0.017, 0.022, mat=1, m=frame(Vector((xr, y, z)), nrm)) G['collars'] = b.finish("DX07_Hull_JointCollars", [M['rubber'], M['steel']], C['Hull'], sharp=40) # --- hoop stiffeners on the hold (regular, rounded, ~8 cm proud) b = B() for xr in (-2.3, -1.55, -0.8, 0.45): prof = [(-0.075, -0.012), (-0.06, 0.04), (-0.035, 0.072), (0.0, 0.08), (0.035, 0.072), (0.06, 0.04), (0.075, -0.012)] secs = [] for dx, g in prof: w, ht, hb, nn, cz = eval_keys(HOLD, xr + dx) secs.append((xr + dx, w + g, ht + g, hb + g, nn, cz)) b.loft(secs, seg=72, mat=0, cap0=None, cap1=None) G['ribs'] = b.finish("DX07_Hull_HoopStiffeners", [M['red']], C['Hull'], sharp=None) def build_drill(M, C): b = B() AX = frame((0, 0, 0), (1, 0, 0), (0, 0, 1)) # local Z -> world +X # flange (bolted mount) b.lathe([(0.55, 3.88), (0.80, 3.88), (0.82, 3.90), (0.82, 3.96), (0.80, 3.98), (0.55, 3.98)], 64, 0, AX) for i in range(18): a = TAU * i / 18 p = Vector((3.875, 0.75 * math.cos(a), 0.75 * math.sin(a))) b.hexbolt(0.022, 0.025, 0, frame(p, (-1, 0, 0), (0, math.cos(a), math.sin(a)))) # hydraulic couplers + short hoses back into the drive module for a in (math.radians(115), math.radians(295)): ca, sa = math.cos(a), math.sin(a) p0 = Vector((3.86, 0.62 * ca, 0.62 * sa)) b.cyl(0.05, 0.1, 16, 0, frame(p0 + Vector((-0.05, 0, 0)), (1, 0, 0))) b.cyl(0.035, 0.05, 6, 0, frame(p0 + Vector((-0.13, 0, 0)), (1, 0, 0))) _, hn = surf(FWD, 3.5, a) hp, _ = surf(FWD, 3.5, a) b.sweep([p0 + Vector((-0.15, 0, 0)), p0 + Vector((-0.28, 0.06 * ca, 0.06 * sa)), hp + hn * 0.06, hp - hn * 0.02], 0.032, seg=12, step=0.02, mat=4) # drive collar (hazard striped) b.lathe([(0.4, 3.98), (0.60, 3.98), (0.635, 4.0), (0.635, 4.16), (0.60, 4.19), (0.4, 4.19)], 64, 1, AX) # cutter base disc b.lathe([(0.3, 4.18), (1.10, 4.19), (1.20, 4.215), (1.235, 4.26), (1.22, 4.31), (1.16, 4.34), (0.3, 4.34)], 96, 2, AX) # ogive cone prof = [] L0, L1, R0 = 4.33, 5.82, 1.12 for i in range(33): u = i / 32 r = max(R0 * (1 - u) ** 0.72, 0.035) prof.append((r, L0 + (L1 - L0) * u)) prof = [(0.25, L0)] + prof b.lathe(prof, 64, 2, AX) def cone_pt(u, th): r = max(R0 * (1 - u) ** 0.72, 0.035) x = L0 + (L1 - L0) * u du = 0.002 r2 = max(R0 * (1 - min(u + du, 1)) ** 0.72, 0.035) drdx = (r2 - r) / ((L1 - L0) * du) radial = Vector((0, math.cos(th), math.sin(th))) p = Vector((x, 0, 0)) + radial * r nrm = (radial - Vector((1, 0, 0)) * drdx).normalized() return p, nrm, radial # three helical flights with carbide picks for f in range(3): th0 = TAU * f / 3 samples = [] n = 90 for i in range(n + 1): u = 0.03 + 0.86 * i / n th = th0 + TAU * 1.05 * u samples.append((u, th) + cone_pt(u, th)) hgt_fn = lambda u: 0.10 * (1 - 0.45 * u) b.begin() bm = b.bm rings = [] for i, (u, th, p, nrm, radial) in enumerate(samples): pn = samples[min(i + 1, n)][2] pp = samples[max(i - 1, 0)][2] tan = (pn - pp).normalized() bi = tan.cross(nrm).normalized() h = hgt_fn(u) t = 0.022 rings.append([bm.verts.new(p - nrm * 0.02 - bi * t), bm.verts.new(p + nrm * h - bi * t), bm.verts.new(p + nrm * h + bi * t), bm.verts.new(p - nrm * 0.02 + bi * t)]) for a, c in zip(rings[:-1], rings[1:]): for k in range(4): k2 = (k + 1) % 4 bm.faces.new((a[k], a[k2], c[k2], c[k])) bm.faces.new(rings[0][::-1]) bm.faces.new(rings[-1]) b.end(2) # picks acc = 0.0 for i in range(1, n + 1): acc += (samples[i][2] - samples[i - 1][2]).length if acc < 0.2: continue acc = 0.0 u, th, p, nrm, radial = samples[i] tang_rot = Vector((0, -math.sin(th), math.cos(th))) d = (nrm * 0.55 + tang_rot * 0.6 + Vector((1, 0, 0)) * 0.25).normalized() base = p + nrm * hgt_fn(u) m = frame(base, d, nrm) b.box(0.07, 0.06, 0.07, 2, m @ Matrix.Translation((0, 0, 0.0))) b.cyl(0.03, 0.05, 12, 3, m @ Matrix.Translation((0, 0, 0.06))) b.cyl(0.03, 0.07, 12, 3, m @ Matrix.Translation((0, 0, 0.12)), r2=0.004) # gauge picks round the disc rim for i in range(22): a = TAU * i / 22 radial = Vector((0, math.cos(a), math.sin(a))) base = Vector((4.3, 0, 0)) + radial * 1.19 d = (radial * 0.55 + Vector((1, 0, 0)) * 0.8).normalized() m = frame(base, d, (1, 0, 0)) b.box(0.08, 0.07, 0.06, 2, m) b.cyl(0.032, 0.05, 12, 3, m @ Matrix.Translation((0, 0, 0.055))) b.cyl(0.032, 0.07, 12, 3, m @ Matrix.Translation((0, 0, 0.115)), r2=0.004) # muck-bucket openings on the cone flank (dark slots between flights) for i in range(6): a = TAU * (i + 0.5) / 6 p, nrm, radial = cone_pt(0.1, a) m = frame(p, nrm, (1, 0, 0)) b.box(0.3, 0.13, 0.03, 5, m) # pilot bit b.cyl(0.06, 0.14, 16, 3, AX @ Matrix.Translation((0, 0, 5.84))) b.cyl(0.06, 0.16, 16, 3, AX @ Matrix.Translation((0, 0, 5.99)), r2=0.004) ob = b.finish("DX07_Drill_CutterHead", [M['steel'], M['hazard'], M['cutter'], M['bit'], M['rubber'], M['void']], C['Drill'], sharp=40, origin=(3.92, 0, 0)) G['drill'] = ob return ob def build_cockpit(M, C): Cc = Vector((2.72, 0.88, 0.95)) R = 1.0 v_main = Vector((1.0, -0.2, -0.12)).normalized() v_land = Vector((0.55, 0.25, -0.8)).normalized() G['eye'] = Cc.copy() G['v_main'] = v_main hole_main = 0.40 hole_land = 0.17 rot = frame(Cc, v_main, (0, 0, 1)) rot_inv = rot.inverted() vl_local = (rot_inv.to_3x3() @ v_land).normalized() def shell(radius, mat, name, inward=False): bm = bmesh.new() bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=48, radius=radius) kill = [] cm = math.cos(math.asin(hole_main)) cl = math.cos(math.asin(hole_land + 0.02)) for v in bm.verts: d = v.co.normalized() if d.z > cm - 0.004 or d.dot(vl_local) > cl: kill.append(v) bmesh.ops.delete(bm, geom=kill, context='VERTS') bmesh.ops.transform(bm, matrix=rot, verts=bm.verts[:]) if inward: bmesh.ops.reverse_faces(bm, faces=bm.faces[:]) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) C['Cockpit'].objects.link(ob) me.materials.append(mat) me.shade_smooth() ob.parent = G['root'] return ob G['sphere'] = shell(R, M['char'], "DX07_Cockpit_PressureSphere") shell(0.955, M['cavity'], "DX07_Cockpit_Interior", inward=True) # viewport frames (conical, bolted) + glass b = B() def window(v, hr, s): m = frame(Cc, v, (0, 0, 1)) ro = hr + 0.14 * s base_h = math.sqrt(max(1 - ro * ro, 0)) prof = [(hr - 0.005, base_h - 0.03), (ro, base_h - 0.03), (ro, base_h + 0.035), (hr + 0.07 * s, base_h + 0.035 + 0.17 * s), (hr + 0.03 * s, base_h + 0.04 + 0.175 * s), (hr - 0.005, base_h + 0.02 + 0.16 * s)] b.lathe(prof, 64, 0, m) n_b = 24 if s > 0.8 else 12 for i in range(n_b): a = TAU * i / n_b rr = (ro + hr + 0.07 * s) / 2 hh = (base_h + 0.035 + base_h + 0.035 + 0.17 * s) / 2 p_loc = Vector((rr * math.cos(a), rr * math.sin(a), hh)) nl = Vector((math.cos(a) * 0.17 * s, math.sin(a) * 0.17 * s, (ro - hr - 0.07 * s))).normalized() p = m @ p_loc nw = (m.to_3x3() @ nl).normalized() b.hexbolt(0.018 * max(s, 0.7), 0.022, 0, frame(p, nw, m.to_3x3() @ Vector((0, 0, 1)))) # glass disc gm = m @ Matrix.Translation((0, 0, base_h + 0.1 * s)) b.cyl(hr + 0.01, 0.025, 48, 1, gm) # painted yellow outline ring on the sphere ring_prof = [] for k in range(4): rr = ro + 0.03 + 0.05 * k / 3 ring_prof.append((rr, math.sqrt(1.0045 - rr * rr))) return m, ring_prof m1, rp1 = window(v_main, hole_main, 1.0) m2, rp2 = window(v_land, hole_land, 0.55) ob = b.finish("DX07_Cockpit_Viewports", [M['steel'], M['glass']], C['Cockpit'], sharp=40) b2 = B() b2.lathe(rp1, 64, 0, m1, closed=False) b2.lathe(rp2, 48, 0, m2, closed=False) b2.finish("DX07_Mark_ViewportOutline", [M['st_yellow']], C['Markings'], sharp=None) # clamp strap over the sphere (YZ plane, slightly aft of centre) + bolted lugs where it meets the hull b = B() xs = Cc.x - 0.30 rr = math.sqrt(R * R - 0.30 * 0.30) m = Matrix.Translation((xs, Cc.y, Cc.z)) @ frame((0, 0, 0), (1, 0, 0), (0, 0, 1)) b.lathe([(rr - 0.01, -0.085), (rr + 0.05, -0.085), (rr + 0.06, -0.07), (rr + 0.06, 0.07), (rr + 0.05, 0.085), (rr - 0.01, 0.085)], 96, 0, m) # lugs: find where strap enters hull prev = None for i in range(361): a = TAU * i / 360 p = Vector((xs, Cc.y + (rr + 0.03) * math.cos(a), Cc.z + (rr + 0.03) * math.sin(a))) ins = inside_hull(p) if prev is not None and ins != prev: radial = Vector((0, math.cos(a), math.sin(a))) tang = Vector((0, -math.sin(a), math.cos(a))) lm = frame(p, radial, (1, 0, 0)) b.box(0.26, 0.34, 0.12, 0, lm) for dx in (-0.08, 0.08): for dy in (-0.11, 0.11): b.hexbolt(0.02, 0.025, 1, lm @ Matrix.Translation((dx, dy, 0.06))) prev = ins for i in range(10): # strap rivets a = math.radians(20 + i * 14) p = Vector((xs, Cc.y + (rr + 0.06) * math.cos(a), Cc.z + (rr + 0.06) * math.sin(a))) radial = Vector((0, math.cos(a), math.sin(a))) for dx in (-0.05, 0.05): b.hexbolt(0.014, 0.015, 1, frame(p + Vector((dx, 0, 0)), radial)) b.finish("DX07_Cockpit_ClampStrap", [M['char'], M['steel']], C['Cockpit'], sharp=40) def build_propulsion(M, C): # swivelling stern thruster pods (L/R) for side, name in ((1, "Port"), (-1, "Stbd")): b = B() pc = Vector((-4.05, 1.62 * side, -0.12)) AX = frame(pc, (-1, 0, 0), (0, 0, 1)) # local +Z points aft duct = [(0.355, -0.52), (0.37, -0.56), (0.405, -0.57), (0.44, -0.52), (0.45, -0.3), (0.44, 0.15), (0.42, 0.42), (0.395, 0.5), (0.375, 0.5), (0.355, 0.44)] b.lathe(duct, 64, 0, AX) b.lathe([(0.354, -0.5), (0.354, 0.44), (0.35, 0.44), (0.35, -0.5)], 64, 1, AX) # hub + spinner hub = [(0.001, -0.62), (0.06, -0.6), (0.11, -0.52), (0.13, -0.35), (0.13, 0.25), (0.1, 0.42), (0.05, 0.5), (0.001, 0.52)] b.lathe(hub, 32, 1, AX, closed=False) # prop blades for i in range(5): a = TAU * i / 5 bm_ = AX @ Matrix.Rotation(a, 4, 'Z') @ Matrix.Translation((0.23, 0, -0.05)) @ Matrix.Rotation(math.radians(35), 4, 'X') b.box(0.22, 0.012, 0.13, 2, bm_) # stator vanes for i in range(4): a = TAU * i / 4 + 0.3 sm = AX @ Matrix.Rotation(a, 4, 'Z') @ Matrix.Translation((0.24, 0, 0.25)) b.box(0.23, 0.02, 0.18, 1, sm) # aft guard grille for i in range(5): dy = -0.3 + i * 0.15 ln = 2 * math.sqrt(max(0.37 ** 2 - dy ** 2, 0.01)) b.box(0.02, ln, 0.02, 2, AX @ Matrix.Translation((dy, 0, 0.47))) # swivel shaft, bearing housing, pylon hp, hn = surf(STERN, -4.0, 0.0 if side > 0 else math.pi) root = hp - hn * 0.05 tip = pc - Vector((0, 0.44 * side, 0)) mid = (root + tip) / 2 ln = (tip - root).length pm = frame(mid, (tip - root), (1, 0, 0)) b.box(0.5, 0.1, ln, 3, pm) b.cyl(0.19, 0.2, 32, 1, frame(hp + hn * 0.08, hn)) b.cyl(0.14, 0.12, 32, 2, frame(hp + hn * 0.22, hn)) for i in range(8): a = TAU * i / 8 fm = frame(hp + hn * 0.08, hn, (1, 0, 0)) b.hexbolt(0.016, 0.02, 2, fm @ Matrix.Translation((0.155 * math.cos(a), 0.155 * math.sin(a), 0.1))) b.finish(f"DX07_Thruster_{name}", [M['red'], M['char'], M['steel'], M['char']], C['Propulsion'], sharp=40, origin=hp + hn * 0.08) # tunnel thrusters: grilled openings (bow lateral, bow vertical, stern lateral, stern vertical) b = B() spots = [(FWD, 2.42, -0.595), (FWD, 2.42, math.pi + 0.595), (FWD, 3.22, math.pi / 2 + 0.38), (FWD, 3.22, -math.pi / 2 - 0.38), (STERN, -3.22, -0.45), (STERN, -3.22, math.pi + 0.45), (STERN, -3.36, math.pi / 2), (STERN, -3.36, -math.pi / 2)] for keys, x, t in spots: p, nrm = surf(keys, x, t) m = frame(p, nrm, (1, 0, 0)) b.lathe([(0.2, -0.04), (0.285, -0.04), (0.29, 0.0), (0.265, 0.035), (0.215, 0.035), (0.2, 0.02)], 48, 0, m) b.cyl(0.205, 0.01, 40, 1, m @ Matrix.Translation((0, 0, 0.004))) for i in range(5): dy = -0.14 + i * 0.07 ln = 2 * math.sqrt(max(0.205 ** 2 - dy ** 2, 0.001)) b.box(0.018, ln, 0.03, 2, m @ Matrix.Translation((dy, 0, 0.022))) b.finish("DX07_TunnelThrusters", [M['steel'], M['void'], M['char']], C['Propulsion'], sharp=40) def build_ore_system(M, C): # intake scoop behind the cutter, starboard-low b = B() cy, cz = -0.93, -0.52 secs = [] for i in range(13): u = i / 12 x = lerp(3.55, 4.10, u) w = lerp(0.29, 0.34, u ** 1.5) h = lerp(0.29, 0.25, u) n = lerp(2.0, 3.6, u) secs.append((x, w, h, h, n, cz, cy)) secs.append((4.10, 0.29, 0.21, 0.21, 3.6, cz, cy)) secs.append((3.90, 0.26, 0.18, 0.18, 3.4, cz, cy)) b.loft(secs, seg=48, mat=0, cap0='ngon', cap1='ngon') b.bm.faces.ensure_lookup_table() # lip faces -> hazard nf = len(b.bm.faces) for f in b.bm.faces[nf - 2 - 48 * 2: nf - 2 - 48]: f.material_index = 1 b.bm.faces[-1].material_index = 2 for i in range(4): yy = cy - 0.24 + i * 0.16 b.box(0.03, 0.025, 0.4, 3, Matrix.Translation((4.03, yy, cz))) b.finish("DX07_Ore_IntakeScoop", [M['char'], M['hazard'], M['void'], M['steel']], C['Details'], sharp=40) # corrugated ore hose looping along the starboard side into the hold ctrl = [(3.60, -0.93, -0.52), (3.20, -1.10, -0.48), (2.50, -1.42, -0.32), (1.70, -1.66, -0.08), (0.90, -1.74, 0.18), (0.30, -1.70, 0.34), (-0.08, -1.56, 0.39), (-0.17, -1.36, 0.39)] fp, fn = surf(HOLD, -0.17, math.pi - 0.36) ctrl[-1] = tuple(fp + fn * 0.22) ctrl[-2] = tuple(fp + fn * 0.42 + Vector((0.1, 0, 0))) def rfn(s, total): k = (s % 0.085) / 0.085 bump = (0.5 - 0.5 * math.cos(TAU * k)) ** 2 r = 0.262 + 0.03 * bump if s < 0.12 or s > total - 0.12: r = 0.27 return r b = B() path, T, total = b.sweep(ctrl, 0.27, seg=28, step=0.0105, mat=0, radius_fn=rfn) G['hose_path'] = path hb = b.finish("DX07_Ore_Hose", [M['rubber']], C['Details'], sharp=None) # end fittings, clamps and saddle struts b = B() p_end = path[-1][0] t_end = (path[-1][0] - path[-4][0]).normalized() p_st = path[0][0] t_st = (path[3][0] - path[0][0]).normalized() for p, t in ((p_end, t_end), (p_st, -t_st)): m = frame(p, t, (1, 0, 0)) b.lathe([(0.26, -0.14), (0.33, -0.14), (0.34, -0.12), (0.34, 0.05), (0.31, 0.08), (0.26, 0.08)], 48, 0, m) for i in range(12): a = TAU * i / 12 b.hexbolt(0.018, 0.02, 0, m @ Matrix.Translation((0.3 * math.cos(a), 0.3 * math.sin(a), 0.08))) # hold fitting doubler m = frame(fp, fn, (1, 0, 0)) b.lathe([(0.3, -0.1), (0.42, -0.1), (0.44, 0.0), (0.42, 0.04), (0.3, 0.04)], 48, 1, m) for s_at in (1.1, 2.4, 3.5): idx = min(range(len(path)), key=lambda i: abs(path[i][1] - s_at)) p = path[idx][0] t = (path[min(idx + 1, len(path) - 1)][0] - path[max(idx - 1, 0)][0]).normalized() m = frame(p, t, (0, 0, 1)) b.lathe([(0.285, -0.06), (0.32, -0.06), (0.325, 0.06), (0.285, 0.06)], 40, 0, m) # strut to hull keys = FWD if p.x > 2.0 else HOLD tt = math.atan2(p.z, p.y) hp, hn = surf(keys, p.x, tt) top = p + (hp - p).normalized() * 0.31 mid = (top + hp) / 2 ln = (top - hp).length + 0.05 b.box(0.07, 0.07, ln, 0, frame(mid, top - hp, (1, 0, 0))) b.cyl(0.08, 0.05, 20, 0, frame(hp + hn * 0.01, hn)) b.finish("DX07_Ore_HoseFittings", [M['steel'], M['red']], C['Details'], sharp=40) def build_trunk(M, C): b = B() xt, yt = 1.42, 0.0 ztop = eval_keys(HOLD, xt)[1] + eval_keys(HOLD, xt)[4] Z = frame((xt, yt, 0), (0, 0, 1), (1, 0, 0)) b.lathe([(0.001, 0.70), (0.52, 0.70), (0.52, ztop + 0.06), (0.49, ztop + 0.09), (0.44, ztop + 0.09), (0.44, ztop + 0.36), (0.50, ztop + 0.37), (0.51, ztop + 0.43), (0.001, ztop + 0.43)], 64, 0, Z, closed=False) # hatch (domed) - separate object with hinge origin b.finish("DX07_LockoutTrunk", [M['char']], C['Details'], sharp=40) b = B() zh = ztop + 0.43 b.lathe([(0.001, zh), (0.50, zh), (0.505, zh + 0.03), (0.47, zh + 0.06), (0.3, zh + 0.1), (0.001, zh + 0.115)], 64, 0, Z, closed=False) # hand wheel wz = zh + 0.2 b.lathe([(0.15, -0.018), (0.19, -0.018), (0.19, 0.018), (0.15, 0.018)], 40, 1, Matrix.Translation((xt, yt, wz))) for i in range(3): a = TAU * i / 3 b.box(0.17, 0.028, 0.025, 1, Matrix.Translation((xt, yt, wz)) @ Matrix.Rotation(a, 4, 'Z') @ Matrix.Translation((0.085, 0, 0))) b.cyl(0.04, 0.12, 16, 1, Matrix.Translation((xt, yt, zh + 0.14))) # hinge hx = xt - 0.52 b.box(0.12, 0.34, 0.1, 1, Matrix.Translation((hx, yt, zh + 0.02))) b.cyl(0.04, 0.44, 16, 1, frame((hx - 0.03, yt, zh + 0.04), (0, 1, 0))) b.finish("DX07_LockoutHatch", [M['red'], M['steel']], C['Details'], sharp=40, origin=(hx - 0.03, yt, zh + 0.04)) # dogs round the top flange b = B() for i in range(8): a = TAU * (i + 0.5) / 8 p = Vector((xt + 0.5 * math.cos(a), yt + 0.5 * math.sin(a), zh - 0.02)) b.box(0.07, 0.05, 0.1, 0, frame(p, (0, 0, 1), (math.cos(a), math.sin(a), 0))) # grab handles either side of the trunk for sy in (-1, 1): p0, n0 = surf(HOLD, 0.95, math.pi / 2 - 0.55 * sy) p1, n1 = surf(HOLD, 1.35, math.pi / 2 - 0.55 * sy) b.sweep([p0 - n0 * 0.02, p0 + n0 * 0.14, p1 + n1 * 0.14, p1 - n1 * 0.02], 0.02, 10, 0.02, 0) b.finish("DX07_LockoutDetails", [M['steel']], C['Details'], sharp=40) G['trunk_xy'] = (xt, yt) def build_misc(M, C): # ---- skids b = B() for sy in (-1, 1): y = 0.74 * sy ctrl = [(-2.35, y, -1.33), (-2.2, y, -1.40), (1.5, y, -1.40), (1.9, y, -1.28), (2.12, y, -1.08)] b.sweep(ctrl, 0.07, 16, 0.04, 0) for xs in (-1.85, -0.3, 1.15): w, ht, hb, n, cz = eval_keys(HOLD, xs) zb = cz - hb * (1 - (abs(y) / w) ** n) ** (1 / n) top = Vector((xs, y * 0.96, zb + 0.08)) bot = Vector((xs, y, -1.38)) b.cyl(0.055, (top - bot).length, 16, 0, frame((top + bot) / 2, top - bot), r2=0.045) b.box(0.26, 0.2, 0.05, 0, Matrix.Translation((xs, y * 0.97, zb + 0.02))) b.finish("DX07_Skids", [M['skid']], C['Details'], sharp=40) # ---- headlights on pan/tilt arms beside the drill b = B() lamps = [] for t, tip in ((2.35, Vector((3.86, -1.02, 1.08))), (-0.82, Vector((3.86, 1.08, -0.92)))): rp, rn = surf(FWD, 3.25, t) b.cyl(0.13, 0.08, 24, 0, frame(rp + rn * 0.02, rn)) elbow = rp + rn * 0.42 b.sweep([rp + rn * 0.05, rp + rn * 0.25, elbow], 0.055, 16, 0.03, 0) b.sphere(0.085, 20, 12, 0, Matrix.Translation(elbow)) b.sweep([elbow, elbow.lerp(tip, 0.5) + rn * 0.04, tip - Vector((0.14, 0, 0))], 0.045, 16, 0.03, 0) d = (Vector((6.5, 0, -0.4)) - tip).normalized() d = (d + Vector((1, 0, 0)) * 2).normalized() m = frame(tip, d, (0, 0, 1)) b.lathe([(0.001, -0.16), (0.1, -0.16), (0.14, -0.1), (0.15, 0.08), (0.135, 0.1), (0.12, 0.08), (0.001, 0.08)], 32, 1, m, closed=False) b.cyl(0.118, 0.01, 32, 2, m @ Matrix.Translation((0, 0, 0.07))) for i in range(3): b.box(0.25, 0.012, 0.02, 0, m @ Matrix.Translation((0, -0.06 + i * 0.06, 0.1))) # hood b.lathe([(0.15, 0.06), (0.17, 0.06), (0.17, 0.2), (0.155, 0.2)], 24, 1, m, closed=False, arc=(0.2, math.pi - 0.2)) lamps.append((tip + d * 0.1, d)) b.finish("DX07_Headlights", [M['steel'], M['char'], M['lamp']], C['Details'], sharp=40) G['lamps'] = lamps # ---- sonar / strobe mast (stern top) b = B() xm = -3.95 w, ht, hb, n, cz = eval_keys(STERN, xm) zb = cz + ht b.lathe([(0.001, zb - 0.1), (0.2, zb - 0.1), (0.2, zb + 0.03), (0.16, zb + 0.06), (0.001, zb + 0.06)], 32, 0, Matrix.Translation((xm, 0, 0)), closed=False) b.cyl(0.04, 0.55, 16, 0, Matrix.Translation((xm, 0, zb + 0.32))) b.cyl(0.13, 0.06, 32, 0, Matrix.Translation((xm, 0, zb + 0.3))) b.cyl(0.06, 0.08, 20, 2, Matrix.Translation((xm, 0, zb + 0.64))) b.sphere(0.06, 20, 12, 1, Matrix.Translation((xm, 0, zb + 0.68))) b.finish("DX07_SonarStrobeMast", [M['steel'], M['glass'], M['strobe']], C['Details'], sharp=40) # ---- lifting padeyes on the hold spine b = B() for xp in (-1.9, 0.0): p, nrm = surf(HOLD, xp, math.pi / 2 + 0.28) m = frame(p, nrm, (1, 0, 0)) b.box(0.34, 0.05, 0.16, 0, m @ Matrix.Translation((0, 0, 0.05))) b.lathe([(0.045, -0.03), (0.075, -0.03), (0.075, 0.03), (0.045, 0.03)], 24, 0, m @ Matrix.Translation((0, 0, 0.16)) @ Matrix.Rotation(math.pi / 2, 4, 'X')) b.box(0.44, 0.24, 0.025, 0, m) # bolt sockets: single spaced rows on the port flank for xs in (-1.93, -1.18, 1.15): for t in (0.42, -0.5): p, nrm = surf(HOLD, xs, t) m = frame(p, nrm, (1, 0, 0)) b.cyl(0.055, 0.02, 20, 0, m) b.cyl(0.036, 0.03, 20, 1, m @ Matrix.Translation((0, 0, 0.005))) # stern tail access ring m = frame((-4.69, 0, 0), (-1, 0, 0), (0, 0, 1)) b.lathe([(0.1, -0.05), (0.2, -0.05), (0.2, 0.02), (0.16, 0.04), (0.1, 0.03)], 40, 0, m) b.cyl(0.1, 0.03, 32, 0, m @ Matrix.Translation((0, 0, 0.02))) for i in range(8): a = TAU * i / 8 b.hexbolt(0.014, 0.015, 0, m @ Matrix.Translation((0.17 * math.cos(a), 0.17 * math.sin(a), 0.03))) # belly dump-door hinges for xd0, xd1 in ((-2.15, -0.9), (-0.65, 0.3), (0.6, 1.55)): for side in (-1, 1): t = -math.pi / 2 + side * 0.62 for xk in (xd0 + 0.15, (xd0 + xd1) / 2, xd1 - 0.15): p, nrm = surf(HOLD, xk, t) b.cyl(0.035, 0.16, 16, 0, frame(p + nrm * 0.02, (1, 0, 0))) b.finish("DX07_HullHardware", [M['steel'], M['void']], C['Details'], sharp=40) def conform_strip(b, keys, x0, x1, t0, t1, nx, nt, off=0.006, mat=0): b.begin() bm = b.bm grid = [] for i in range(nx + 1): row = [] for j in range(nt + 1): x = lerp(x0, x1, i / nx) t = lerp(t0, t1, j / nt) p, nrm = surf(keys, x, t) row.append(bm.verts.new(p + nrm * off)) grid.append(row) for i in range(nx): for j in range(nt): bm.faces.new((grid[i][j], grid[i + 1][j], grid[i + 1][j + 1], grid[i][j + 1])) b.end(mat) def text_mesh(body, size, name, target, loc, nrm, read_dir, mat, col, extrude=0.0, align='CENTER', offset=0.004): cu = bpy.data.curves.new(name + "_txt", 'FONT') cu.body = body cu.size = size cu.align_x = align cu.align_y = 'CENTER' cu.extrude = extrude if os.path.exists(FONT_PATH): fnt = bpy.data.fonts.load(FONT_PATH, check_existing=True) cu.font = fnt tob = bpy.data.objects.new(name + "_tmp", cu) bpy.context.scene.collection.objects.link(tob) dg = bpy.context.evaluated_depsgraph_get() dg.update() me = bpy.data.meshes.new_from_object(tob.evaluated_get(dg)) bpy.data.objects.remove(tob, do_unlink=True) bpy.data.curves.remove(cu) bm = bmesh.new() bm.from_mesh(me) bmesh.ops.triangulate(bm, faces=bm.faces[:]) for _ in range(2): bmesh.ops.subdivide_edges(bm, edges=[e for e in bm.edges if e.calc_length() > size * 0.12], cuts=1, use_grid_fill=True) bmesh.ops.triangulate(bm, faces=bm.faces[:]) m = frame(Vector(loc) + Vector(nrm) * 0.03, nrm, read_dir) bmesh.ops.transform(bm, matrix=m, verts=bm.verts[:]) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) col.objects.link(ob) me.materials.append(mat) me.shade_flat() sw = ob.modifiers.new("Conform", 'SHRINKWRAP') sw.target = target sw.wrap_method = 'NEAREST_SURFACEPOINT' sw.offset = offset ob.parent = G['root'] return ob def build_markings(M, C): col = C['Markings'] hold = G['hold'] # diver walkway (non-slip, yellow edged) on the port side of the spine b = B() t_c, half = math.pi / 2 - 0.26, 0.2 conform_strip(b, HOLD, -2.55, 1.0, t_c - half, t_c - half + 0.045, 80, 1, mat=1) conform_strip(b, HOLD, -2.55, 1.0, t_c - half + 0.045, t_c + half - 0.045, 80, 4, mat=0) conform_strip(b, HOLD, -2.55, 1.0, t_c + half - 0.045, t_c + half, 80, 1, mat=1) b.finish("DX07_Mark_Walkway", [M['grit'], M['st_yellow']], col, sharp=None) # belly door seams b = B() for xd0, xd1 in ((-2.15, -0.9), (-0.65, 0.3), (0.6, 1.55)): tl, tr = -math.pi / 2 - 0.62, -math.pi / 2 + 0.62 conform_strip(b, HOLD, xd0, xd1, -math.pi / 2 - 0.006, -math.pi / 2 + 0.006, 30, 1, off=0.003) conform_strip(b, HOLD, xd0, xd1, tl - 0.008, tl + 0.008, 30, 1, off=0.003) conform_strip(b, HOLD, xd0, xd1, tr - 0.008, tr + 0.008, 30, 1, off=0.003) for xx in (xd0, xd1): conform_strip(b, HOLD, xx - 0.008, xx + 0.008, tl, tr, 1, 30, off=0.003) b.finish("DX07_Mark_BellyDoorSeams", [M['seam']], col, sharp=None) # yellow outline around lockout trunk base b = B() xt, yt = G['trunk_xy'] b.begin() bm = b.bm rings = [] for rr in (0.56, 0.6): ring = [] for i in range(64): a = TAU * i / 64 x, y = xt + rr * math.cos(a), yt + rr * math.sin(a) w, ht, hb, n, cz = eval_keys(HOLD, x) z = cz + ht * max(1 - (abs(y) / w) ** n, 0) ** (1 / n) + 0.006 ring.append(bm.verts.new((x, y, z))) rings.append(ring) for i in range(64): j = (i + 1) % 64 bm.faces.new((rings[0][i], rings[0][j], rings[1][j], rings[1][i])) b.end(0) b.finish("DX07_Mark_TrunkOutline", [M['st_yellow']], col, sharp=None) PORT, STBD, FWDX = (-1, 0, 0), (1, 0, 0), (1, 0, 0) # oversized worn hull number (port flank, clear gap between stiffeners) p, nrm = surf(HOLD, -0.18, -0.06) text_mesh("07", 0.95, "DX07_Mark_HullNumber", hold, p, nrm, PORT, M['st_white'], col) # bay numbers along the upper port shoulder for i, xb in enumerate((-2.62, -1.93, -1.18, -0.18, 1.2)): p, nrm = surf(HOLD, xb, 0.98) text_mesh(f"{i + 1:02d}", 0.14, f"DX07_Mark_Bay{i + 1:02d}", hold, p, nrm, PORT, M['st_yellow'], col) # boxed ID plate (yellow plate + black text) p, nrm = surf(HOLD, -1.18, 0.18) b = B() tc = 0.18 conform_strip(b, HOLD, -1.18 - 0.3, -1.18 + 0.3, tc - 0.11, tc + 0.11, 20, 8, off=0.005) b.finish("DX07_Mark_IDPlate", [M['st_yellow']], col, sharp=None) p1, n1 = surf(HOLD, -1.18, tc + 0.045) text_mesh("DX-07 ORE HAULER", 0.062, "DX07_Mark_IDText1", hold, p1, n1, PORT, M['st_black'], col, offset=0.008) p2, n2 = surf(HOLD, -1.18, tc - 0.04) text_mesh("HOLD 38 t RATED 6200 m", 0.046, "DX07_Mark_IDText2", hold, p2, n2, PORT, M['st_black'], col, offset=0.008) # lift / mount / lockout callouts for xp in (-1.9, 0.0): p, nrm = surf(HOLD, xp, math.pi / 2 + 0.62) text_mesh("LIFT HERE", 0.07, f"DX07_Mark_Lift_{xp}", hold, p, nrm, STBD, M['st_white'], col) p, nrm = surf(FWD, 3.62, 0.25) text_mesh("STD-MOUNT A", 0.075, "DX07_Mark_MountA", G['fwd'], p, nrm, PORT, M['st_white'], col) p, nrm = surf(FWD, 3.55, math.pi - 0.1) text_mesh("350 BAR", 0.06, "DX07_Mark_Hydraulic", G['fwd'], p, nrm, STBD, M['st_yellow'], col) p, nrm = surf(STERN, -3.22, -0.2) text_mesh("KEEP CLEAR", 0.05, "DX07_Mark_Thruster", G['stern'], p, nrm, PORT, M['st_yellow'], col) p, nrm = surf(STERN, -3.6, 0.35) text_mesh("DX-07", 0.16, "DX07_Mark_SternID", G['stern'], p, nrm, PORT, M['st_white'], col) # =============================================================== scene / render def setup_collections(): root = bpy.data.collections.get(COLL) if root: for o in list(root.all_objects): bpy.data.objects.remove(o, do_unlink=True) for c in reversed(list(root.children_recursive)): bpy.data.collections.remove(c) else: root = bpy.data.collections.new(COLL) bpy.context.scene.collection.children.link(root) C = {} for n in ("Hull", "Drill", "Cockpit", "Propulsion", "Details", "Markings"): c = bpy.data.collections.new(f"DX07_{n}") root.children.link(c) C[n] = c e = bpy.data.objects.new("DX07_ROOT", None) e.empty_display_type = 'ARROWS' e.empty_display_size = 1.5 root.objects.link(e) G['root'] = e return C def build(): for n in ("Cube", "Light", "Camera"): o = bpy.data.objects.get(n) if o: bpy.data.objects.remove(o, do_unlink=True) C = setup_collections() bpy.data.orphans_purge(do_recursive=True) M = build_materials() build_hull(M, C) build_drill(M, C) build_cockpit(M, C) build_propulsion(M, C) build_ore_system(M, C) build_trunk(M, C) build_misc(M, C) build_markings(M, C) return {"objects": len(bpy.data.collections[COLL].all_objects)} def setup_render(res=(1600, 900), samples=96): sc = bpy.context.scene sc.render.engine = 'CYCLES' prefs = bpy.context.preferences.addons['cycles'].preferences try: prefs.compute_device_type = 'HIP' prefs.get_devices() for d in prefs.devices: d.use = True sc.cycles.device = 'GPU' except Exception: sc.cycles.device = 'CPU' sc.cycles.samples = samples sc.cycles.use_denoising = True sc.cycles.use_adaptive_sampling = True sc.render.resolution_x, sc.render.resolution_y = res sc.render.resolution_percentage = 100 sc.render.image_settings.file_format = 'PNG' sc.view_settings.view_transform = 'AgX' try: sc.view_settings.look = 'AgX - Medium High Contrast' except Exception: pass # world: studio HDRI for reflections, flat backdrop for camera w = sc.world or bpy.data.worlds.new("DX07_World") sc.world = w w.use_nodes = True nt = w.node_tree nt.nodes.clear() out = nt.nodes.new('ShaderNodeOutputWorld') env = nt.nodes.new('ShaderNodeTexEnvironment') hdr = os.path.join(bpy.utils.system_resource('DATAFILES', path='studiolights/world'), 'studio.exr') env.image = bpy.data.images.load(hdr, check_existing=True) bg1 = nt.nodes.new('ShaderNodeBackground') bg1.inputs['Strength'].default_value = 0.55 nt.links.new(env.outputs['Color'], bg1.inputs['Color']) bg2 = nt.nodes.new('ShaderNodeBackground') bg2.inputs['Color'].default_value = (0.26, 0.275, 0.29, 1) lp = nt.nodes.new('ShaderNodeLightPath') mix = nt.nodes.new('ShaderNodeMixShader') nt.links.new(lp.outputs['Is Camera Ray'], mix.inputs[0]) nt.links.new(bg1.outputs[0], mix.inputs[1]) nt.links.new(bg2.outputs[0], mix.inputs[2]) nt.links.new(mix.outputs[0], out.inputs['Surface']) # lights lc = bpy.data.collections.get("DX07_Studio") or bpy.data.collections.new("DX07_Studio") if lc.name not in bpy.context.scene.collection.children: bpy.context.scene.collection.children.link(lc) for o in list(lc.objects): bpy.data.objects.remove(o, do_unlink=True) def area(name, loc, target, power, size, color): ld = bpy.data.lights.new(name, 'AREA') ld.energy = power ld.size = size ld.color = color ob = bpy.data.objects.new(name, ld) lc.objects.link(ob) ob.location = loc d = Vector(target) - Vector(loc) ob.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler() return ob area("Key", (6, 9, 8), (0, 0, 0), 3200, 5, (1.0, 0.95, 0.88)) area("Rim", (-9, -6, 6), (0, 0, 0), 3800, 4, (0.75, 0.85, 1.0)) area("Fill", (7, -8, -2), (0, 0, 0), 900, 6, (0.9, 0.93, 1.0)) area("Top", (0, 0, 10), (0, 0, 0), 1200, 8, (1, 1, 1)) for i, (p, d) in enumerate(G.get('lamps', [])): ld = bpy.data.lights.new(f"Headlamp{i}", 'SPOT') ld.energy = 600 ld.spot_size = math.radians(45) ld.spot_blend = 0.6 ld.color = (1.0, 0.9, 0.75) ob = bpy.data.objects.new(f"Headlamp{i}", ld) lc.objects.link(ob) ob.location = p ob.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler() # cameras cams = { "Cam_34Front": ((10.5, 8.0, 3.6), (0.6, 0, -0.15), 50), "Cam_34Rear": ((-9.8, -7.8, 3.4), (0.2, 0, -0.1), 50), "Cam_Side": ((0.5, 15.5, 0.3), (0.5, 0, 0.0), 50), "Cam_Front": ((12.5, -3.0, 1.6), (1.5, 0, 0.0), 55), "Cam_Top": ((0.6, 0.0, 16.0), (0.6, 0.0, 0.0), 50), "Cam_StbdLow": ((6.0, -10.0, -2.6), (0.4, 0, 0.0), 45), } for n, (loc, tgt, lens) in cams.items(): cd = bpy.data.cameras.get(n) or bpy.data.cameras.new(n) cd.lens = lens ob = bpy.data.objects.get(n) or bpy.data.objects.new(n, cd) if ob.name not in lc.objects: lc.objects.link(ob) ob.location = loc d = Vector(tgt) - Vector(loc) ob.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler() # first-person view from inside the pressure sphere cd = bpy.data.cameras.get("Cam_Pilot") or bpy.data.cameras.new("Cam_Pilot") cd.lens = 16 cd.clip_start = 0.02 ob = bpy.data.objects.get("Cam_Pilot") or bpy.data.objects.new("Cam_Pilot", cd) if ob.name not in lc.objects: lc.objects.link(ob) eye = G.get('eye', Vector((2.72, 0.88, 0.95))) ob.location = eye + Vector((0.1, 0, 0.05)) d = G.get('v_main', Vector((1, -0.2, -0.12))) ob.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler() return "ok" def render_views(folder, names=None, samples=None): sc = bpy.context.scene if samples: sc.cycles.samples = samples os.makedirs(folder, exist_ok=True) names = names or ["Cam_34Front", "Cam_34Rear", "Cam_Side", "Cam_Front", "Cam_Top", "Cam_StbdLow", "Cam_Pilot"] out = [] for n in names: sc.camera = bpy.data.objects[n] sc.render.filepath = os.path.join(folder, n + ".png") bpy.ops.render.render(write_still=True) out.append(sc.render.filepath) return out