"""Preview renders and the fit check against the real Kazah Tender. Nothing is saved: the Kazah is only linked into this session as a reference and is not part of the model file. blender.exe -b --python-exit-code 1 --python render_previews.py [-- view names] """ import bpy, math, os, sys, json from mathutils import Vector, Matrix from mathutils.bvhtree import BVHTree HERE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.dirname(HERE) PREV = os.path.join(OUT, "previews") KAZAH = r"D:\HADAL - The Descent\models\Kazah-stern-cavity-fixedv4.blend" ONLY = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] bpy.ops.wm.open_mainfile(filepath=KAZAH) kz_names = [o.name for o in bpy.context.scene.objects if o.type == 'MESH' and o.visible_get()] bpy.ops.wm.open_mainfile(filepath=os.path.join(OUT, "DockArm_Carousel.blend")) sc = bpy.context.scene with bpy.data.libraries.load(KAZAH, link=True) as (src, dst): dst.objects = [n for n in src.objects if n in kz_names] kz = bpy.data.collections.new("_Kazah_reference") for o in dst.objects: if o is not None: kz.objects.link(o) C_REF = bpy.data.collections.new("_Reference") sc.collection.children.link(C_REF) STOWED_Z, CRADLE_R = -1.0, 6.0 arms = [bpy.data.objects["Arm%d" % (k + 1)] for k in range(3)] tenders = [] for k in range(3): th = math.radians(-90.0 - 120.0 * k) e = bpy.data.objects.new("_Tender%d" % (k + 1), None) e.instance_type = 'COLLECTION' e.instance_collection = kz e.rotation_euler = (0, 0, math.radians(90.0 - 120.0 * k)) # Kazah file: nose -Y, port +X -> nose tangential, starboard outward e["th"] = th C_REF.objects.link(e) tenders.append(e) def pose(k, lower, claw=0.0, tender=True): arms[k]["lower_m"] = float(lower) arms[k]["claw_open"] = float(claw) arms[k].update_tag() th = tenders[k]["th"] tenders[k].location = (CRADLE_R * math.cos(th), CRADLE_R * math.sin(th), STOWED_Z - lower) tenders[k].hide_render = not tender tenders[k].hide_viewport = not tender bpy.context.view_layer.update() # ------------------------------------------------------------------ fit check against the Kazah mesh def tender_geo(k, canopy_open_deg=0.0): dg = bpy.context.evaluated_depsgraph_get() V, F = [], [] hinge = tenders[k].matrix_world @ Vector((0.0, -1.207, 1.24)) # Tender (x 1.207, z 1.24) in Kazah file axes ax = tenders[k].matrix_world.to_3x3() @ Vector((1, 0, 0)) # Kazah file X = the Tender's lateral axis R = Matrix.Translation(hinge) @ Matrix.Rotation(math.radians(canopy_open_deg), 4, ax) @ Matrix.Translation(-hinge) for it in dg.object_instances: if not (it.is_instance and it.parent and it.parent.original == tenders[k] and it.object.type == 'MESH'): continue me = it.object.data me.calc_loop_triangles() M = it.matrix_world.copy() if canopy_open_deg and it.object.name.split(".")[0] in ("Canopy_Lid", "Canopy_Glass"): M = R @ M b = len(V) V += [M @ v.co for v in me.vertices] F += [tuple(b + i for i in t.vertices) for t in me.loop_triangles] return V, F def obj_geo(o): dg = bpy.context.evaluated_depsgraph_get() e = o.evaluated_get(dg) me = e.data me.calc_loop_triangles() M = e.matrix_world.copy() return [M @ v.co for v in me.vertices], [tuple(t.vertices) for t in me.loop_triangles] def fit(k, names, canopy_open_deg=0.0): TV, TF = tender_geo(k, canopy_open_deg) tb = BVHTree.FromPolygons(TV, TF) rows = [] for n in names: o = bpy.data.objects[n] V, F = obj_geo(o) ob = BVHTree.FromPolygons(V, F) d1 = min(((tb.find_nearest(v)[3], v) for v in V), key=lambda t: t[0]) lo = Vector((min(v.x for v in V), min(v.y for v in V), min(v.z for v in V))) - Vector((0.3, 0.3, 0.3)) hi = Vector((max(v.x for v in V), max(v.y for v in V), max(v.z for v in V))) + Vector((0.3, 0.3, 0.3)) near = [v for v in TV if lo.x < v.x < hi.x and lo.y < v.y < hi.y and lo.z < v.z < hi.z] d2 = min((ob.find_nearest(v)[3] for v in near), default=9.0) rows.append(dict(part=n, crossing_triangle_pairs=len(tb.overlap(ob)), min_gap_m=round(min(d1[0], d2), 4))) return rows fitrep = {} for k in range(3): pose(k, 0.0) A1 = ["Arm1_Head"] + ["Arm1_Claw%d" % j for j in (1, 2, 3, 4)] fitrep["stowed, claws closed: head and claws against the Kazah"] = fit(0, A1) fitrep["stowed: turret, links, ore hose against the Kazah"] = fit(0, ["SM_DockArm_Turret", "Arm1_UpperLink", "Arm1_LowerLink", "Arm1_OreHose"]) pose(0, 5.3) fitrep["BOARD, canopy opened 55 deg about the top of its rear hoop: arm 1 against the Kazah"] = fit(0, A1 + ["Arm1_UpperLink", "Arm1_LowerLink", "Arm1_OreHose"], canopy_open_deg=-55.0) pose(0, 2.5) fitrep["half way down: links, ore hose against the Kazah"] = fit(0, ["Arm1_UpperLink", "Arm1_LowerLink", "SM_DockArm_Turret", "Arm1_OreHose"]) pose(0, 0.0) json.dump(fitrep, open(os.path.join(HERE, "fit_report.json"), "w"), indent=1) for key, rows in fitrep.items(): print("FIT", key) for r in rows: print("FIT ", r) # ------------------------------------------------------------------ set dressing (this session only) def plane(name, z, r, col, rough=0.6): me = bpy.data.meshes.new(name) me.from_pydata([(-r, -r, z), (r, -r, z), (r, r, z), (-r, r, z)], [], [(0, 1, 2, 3)]) m = bpy.data.materials.new(name) m.use_nodes = True b = m.node_tree.nodes["Principled BSDF"] b.inputs["Base Color"].default_value = col + (1,) b.inputs["Roughness"].default_value = rough m.diffuse_color = col + (1,) me.materials.append(m) o = bpy.data.objects.new(name, me) C_REF.objects.link(o) return o ceiling = plane("_ceiling", 3.03, 14, (0.42, 0.44, 0.46)) # the vessel interior's bay roof (the hose needs more than the blockout's 1.20) import bmesh as _bm _pm = bpy.data.meshes.new("_pylon") _b = _bm.new() _bm.ops.create_cone(_b, cap_ends=True, segments=40, radius1=1.2, radius2=1.2, depth=1.83, matrix=Matrix.Translation((0, 0, 1.2 + 0.915))) _b.to_mesh(_pm); _b.free() for _p in _pm.polygons: _p.use_smooth = abs(_p.normal.z) < 0.5 # round wall, flat ends _pm.materials.append(ceiling.data.materials[0]) C_REF.objects.link(bpy.data.objects.new("_pylon", _pm)) deck = plane("_deck", -7.5, 20, (0.10, 0.11, 0.12), 0.7) cam = sc.camera def look(loc, tgt, lens): cam.location = loc cam.rotation_euler = (Vector(tgt) - Vector(loc)).to_track_quat('-Z', 'Y').to_euler() cam.data.lens = lens cam.data.clip_end = 200 def render(name): sc.render.filepath = os.path.join(PREV, name + ".png") bpy.ops.render.render(write_still=True) print("RENDERED", name) # Cycles (the user, 2026-10-13: "do it more realistic with good textures"): roughness and normal maps only show in # reflections and bounced light. The surroundings that the metal mirrors come from Blender's own bundled studio light # (interior.exr, in its install folder); the camera itself sees a plain dark backdrop. This session only, not saved. sc.render.engine = 'CYCLES' try: _prefs = bpy.context.preferences.addons['cycles'].preferences for _kind in ('OPTIX', 'CUDA', 'HIP', 'ONEAPI'): try: _prefs.compute_device_type = _kind except Exception: continue (_prefs.refresh_devices if hasattr(_prefs, "refresh_devices") else _prefs.get_devices)() if any(d.type == _kind for d in _prefs.devices): for d in _prefs.devices: d.use = d.type == _kind sc.cycles.device = 'GPU' break except Exception as ex: print("GPU not set:", ex) sc.cycles.samples = int(os.environ.get("DOCKARM_SAMPLES", "192")) sc.cycles.use_denoising = True sc.cycles.transparent_max_bounces = 16 sc.render.resolution_percentage = int(os.environ.get("DOCKARM_PERCENT", "100")) _hdr = os.path.join(bpy.utils.system_resource('DATAFILES'), "studiolights", "world", "interior.exr") if os.path.isfile(_hdr): _nt = sc.world.node_tree _nt.nodes.clear() _env = _nt.nodes.new('ShaderNodeTexEnvironment'); _env.image = bpy.data.images.load(_hdr) _bg1 = _nt.nodes.new('ShaderNodeBackground'); _bg1.inputs["Strength"].default_value = 0.55 _hs = _nt.nodes.new('ShaderNodeHueSaturation'); _hs.inputs["Saturation"].default_value = 0.2 # its warm wood tones tinted the deck yellow _nt.links.new(_env.outputs["Color"], _hs.inputs["Color"]) _nt.links.new(_hs.outputs["Color"], _bg1.inputs["Color"]) _bg2 = _nt.nodes.new('ShaderNodeBackground'); _bg2.inputs["Color"].default_value = (0.045, 0.05, 0.058, 1); _bg2.inputs["Strength"].default_value = 1.0 _lp = _nt.nodes.new('ShaderNodeLightPath') _mx = _nt.nodes.new('ShaderNodeMixShader') _nt.links.new(_lp.outputs["Is Camera Ray"], _mx.inputs[0]); _nt.links.new(_bg1.outputs[0], _mx.inputs[1]); _nt.links.new(_bg2.outputs[0], _mx.inputs[2]) _wo = _nt.nodes.new('ShaderNodeOutputWorld') _nt.links.new(_mx.outputs[0], _wo.inputs["Surface"]) want = lambda n: (not ONLY) or n in ONLY if want("stowed_below"): for k in range(3): pose(k, 0.0) ceiling.hide_render = False; deck.hide_render = True look((9.0, -11.5, -5.2), (0.2, -0.8, 0.3), 27) render("DockArm_stowed_from_below") if want("board_side"): pose(0, 5.3) ceiling.hide_render = False; deck.hide_render = False look((14.5, -13.0, -1.6), (0.8, -3.6, -2.9), 30) render("DockArm_arm1_at_board") if want("head_close"): pose(0, 5.3) look((2.9, -8.6, -3.7), (0.15, -6.0, -4.75), 42) render("DockArm_head_on_hatch_collar") if want("mechanism"): pose(0, 2.6, 0.0, tender=False); pose(1, 0.0, 0.0, tender=False); pose(2, 5.3, 1.0, tender=False) ceiling.hide_render = True; deck.hide_render = True look((8.5, -10.0, 5.8), (0.0, -1.0, -1.4), 30) render("DockArm_mechanism_no_tenders") look((6.5, -9.5, -4.6), (0.4, -3.2, -0.9), 30) render("DockArm_mechanism_from_below") if want("hose"): for k in range(3): pose(k, 0.0) pose(0, 5.3) ceiling.hide_render = False; deck.hide_render = False look((-10.5, -9.5, -1.2), (-0.3, -4.2, -1.9), 30) # the hose side of the lowered arm render("DockArm_hose_arm1_at_board") look((-3.4, -2.6, -0.9), (-0.75, -4.75, -1.75), 32) render("DockArm_hose_elbow_at_board") look((-3.0, -8.4, -3.3), (-0.5, -6.0, -4.45), 35) render("DockArm_hose_head_coupling") look((-4.4, -9.8, 2.6), (-0.4, -5.6, 0.9), 32) render("DockArm_hose_tip_from_above") look((-8.9, 7.2, 2.7), (-4.6, 3.2, 0.9), 30) # a stowed arm from above: the folded hose render("DockArm_hose_stowed_from_above") look((2.6, -3.0, -0.9), (0.5, -0.9, 0.75), 30) render("DockArm_hose_hub_ports") pose(0, 2.6) look((-9.5, -8.0, 0.4), (-0.3, -3.6, -0.6), 30) render("DockArm_hose_arm1_half_way") if want("closeups"): pose(0, 2.6, 0.0, tender=False); pose(1, 0.0, 0.0, tender=False); pose(2, 5.3, 1.0, tender=False) ceiling.hide_render = False; deck.hide_render = True look((1.9, -2.2, -0.95), (0.05, -3.3, -0.3), 35) render("DockArm_closeup_elbow") look((-7.32, 2.48, 0.05), (-5.24, 2.92, 0.80), 35) render("DockArm_closeup_tip_stowed") look((2.3, -2.6, -0.9), (0.3, -0.6, 0.75), 30) render("DockArm_closeup_hub") if want("wire"): pose(0, 2.6, 0.0, tender=False); pose(1, 0.0, 0.0, tender=False); pose(2, 5.3, 1.0, tender=False) ceiling.hide_render = True; deck.hide_render = True blk = bpy.data.materials.new("_wire") blk.diffuse_color = (0.02, 0.02, 0.02, 1) for me in {o.data for o in bpy.data.collections["DockArm_Assembly"].objects if o.type == 'MESH'}: me.materials.append(blk) for o in bpy.data.collections["DockArm_Assembly"].objects: if o.type == 'MESH': md = o.modifiers.new("_wire", 'WIREFRAME') md.use_replace = False md.thickness = 0.004 md.material_offset = 100 sc.render.engine = 'BLENDER_WORKBENCH' sc.display.shading.light = 'FLAT' sc.display.shading.color_type = 'MATERIAL' sc.display.render_aa = '8' w = sc.world w.color = (0.55, 0.57, 0.6) look((4.6, -8.6, -2.6), (0.3, -4.3, -0.9), 32) render("DockArm_topology_wireframe") print("PREVIEWS DONE")