diff options
Diffstat (limited to 'stage/blender')
| -rw-r--r-- | stage/blender/probe.py | 152 | ||||
| -rw-r--r-- | stage/blender/ship.py | 207 |
2 files changed, 343 insertions, 16 deletions
diff --git a/stage/blender/probe.py b/stage/blender/probe.py index aaffaf4..9b9eb34 100644 --- a/stage/blender/probe.py +++ b/stage/blender/probe.py @@ -563,6 +563,129 @@ def apply_material_mode(bpy, materials, mode): obj.data.materials.append(material) +def merge_details(bpy, master, extras): + """Join detail meshes into the master object so the exported GLB keeps + exactly the contract object names while carrying real secondary detail.""" + if not extras: + return master + for obj in bpy.context.selected_objects: + obj.select_set(False) + for extra in extras: + extra.select_set(True) + master.select_set(True) + bpy.context.view_layer.objects.active = master + bpy.ops.object.join() + return master + + +def detail_torus(bpy, name, location, major_radius, minor_radius, rotation=(0.0, 0.0, 0.0), minor_segments=10): + bpy.ops.mesh.primitive_torus_add( + major_radius=major_radius, + minor_radius=minor_radius, + major_segments=48, + minor_segments=minor_segments, + location=location, + rotation=rotation, + ) + obj = bpy.context.object + obj.name = name + return obj + + +def detail_cube(bpy, name, location, dimensions, rotation=(0.0, 0.0, 0.0)): + bpy.ops.mesh.primitive_cube_add(location=location, rotation=rotation) + obj = bpy.context.object + obj.name = name + obj.dimensions = dimensions + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + return obj + + +def detail_cylinder(bpy, name, location, radius, depth, rotation=(0.0, 0.0, 0.0), vertices=20): + bpy.ops.mesh.primitive_cylinder_add( + vertices=vertices, + radius=radius, + depth=depth, + location=location, + rotation=rotation, + ) + obj = bpy.context.object + obj.name = name + return obj + + +def build_details(bpy, object_id, master, materials): + """Secondary engineering detail per contract object, joined into the + master mesh before export; detail names never reach the GLB.""" + details = [] + + if object_id == "probe.bus.electronics.decagon": + # Panel seams around the decagon bus + mounting bosses on both faces. + for index in range(10): + angle = 2.0 * math.pi * index / 10.0 + y = 0.455 * math.cos(angle) + z = 0.455 * math.sin(angle) + details.append(detail_cube(bpy, f"{master.name}.detail.seam.{index:02d}", (0.0, y, z), (0.50, 0.03, 0.03), rotation=(angle, 0.0, 0.0))) + for offset in (-0.24, 0.24): + details.append(detail_cylinder(bpy, f"{master.name}.detail.boss.{offset:+.2f}", (offset, 0.0, 0.0), 0.30, 0.03, rotation=(0.0, math.pi / 2.0, 0.0))) + elif object_id.startswith("probe.bus.bay.panel."): + index = int(object_id.rsplit(".", 1)[-1]) + angle = 2.0 * math.pi * index / 10.0 + y = 0.46 * math.cos(angle) + z = 0.46 * math.sin(angle) + # Fastener posts and a hinge rib per bay panel. + details.append(detail_cylinder(bpy, master.name + ".detail.hinge", (0.10, y, z), 0.018, 0.30, rotation=(angle, 0.0, 0.0), vertices=8)) + details.append(detail_cube(bpy, master.name + ".detail.latch", (-0.14, y * 1.02, z * 1.02), (0.05, 0.04, 0.10), rotation=(angle, 0.0, 0.0))) + elif object_id.startswith("probe.thruster."): + index = int(object_id.rsplit(".", 1)[-1]) + y, z = ((-0.30, -0.25), (-0.30, 0.25), (0.30, -0.25), (0.30, 0.25))[index - 1] + # Thruster mount plate + feed line to the bus core. + details.append(detail_cube(bpy, master.name + ".detail.mount", (-0.24, y, z), (0.10, 0.12, 0.12))) + details.append(detail_cylinder(bpy, master.name + ".detail.feed", (-0.24, y * 0.55, z * 0.55), 0.014, 0.5, vertices=8)) + elif object_id.startswith("probe.rtg.") and object_id.endswith(tuple(f".fin.{i:02d}" for i in range(3))): + rtg_part, fin_part = object_id.rsplit(".fin.", 1) + rtg_index = int(rtg_part.rsplit(".", 1)[-1]) + fin_index = int(fin_part) + y = (-0.80, -1.22, -1.64)[rtg_index - 1] - 0.09 + fin_index * 0.09 + details.append(detail_torus(bpy, master.name + ".detail.collar", (-0.12, y, -0.16), 0.118, 0.010, rotation=(math.pi / 2.0, 0.0, 0.0), minor_segments=10)) + elif object_id == "probe.rtg.boom": + # Boom truss cross-bracing between the three RTG housings. + for index in range(3): + y = -0.98 - index * 0.42 + details.append(detail_torus(bpy, f"{master.name}.detail.brace.{index:02d}", (-0.12, y, -0.16), 0.035, 0.008, minor_segments=10)) + elif object_id.startswith("probe.rtg."): + index = int(object_id.rsplit(".", 1)[-1]) + y = (-0.80, -1.22, -1.64)[index - 1] + # Radiator fins and end caps along each RTG housing. + for offset in (-0.09, 0.0, 0.09): + details.append(detail_torus(bpy, f"{master.name}.detail.fins.{offset:+.2f}", (-0.12, y + offset, -0.16), 0.115, 0.008, rotation=(math.pi / 2.0, 0.0, 0.0), minor_segments=10)) + elif object_id == "probe.mag.head": + details.append(detail_torus(bpy, master.name + ".detail.collar", (0.0, 6.50, 0.18), 0.105, 0.014, rotation=(math.pi / 2.0, 0.0, 0.0), minor_segments=10)) + elif object_id == "probe.hga.reflector": + # Back-side ribs across the dish so the reflector reads as a rigid + # structure, not a floating surface. + for index in range(6): + angle = 2.0 * math.pi * index / 6.0 + y = 0.55 * math.cos(angle) + z = 0.55 * math.sin(angle) + details.append(detail_cube(bpy, f"{master.name}.detail.rib.{index:02d}", (0.22, y * 0.55, z * 0.55), (0.06, 0.04, 0.04))) + details.append(detail_cylinder(bpy, master.name + ".detail.hub", (0.30, 0.0, 0.0), 0.10, 0.06, rotation=(0.0, math.pi / 2.0, 0.0))) + elif object_id == "probe.scan.platform": + for offset in (-0.12, 0.0, 0.12): + details.append(detail_cylinder(bpy, f"{master.name}.detail.bolt.{offset:+.2f}", (0.0, offset, 1.67), 0.014, 0.02, vertices=8)) + details.append(detail_torus(bpy, master.name + ".detail.ring", (0.0, 0.0, 1.665), 0.16, 0.012, minor_segments=10)) + elif object_id.startswith("probe.pws.tip."): + side = object_id.rsplit(".", 1)[-1] + sign = -1.0 if side == "left" else 1.0 + details.append(detail_cylinder(bpy, master.name + ".detail.cap", (-0.12, sign * 2.50, 1.10), 0.020, 0.03, rotation=(math.pi / 2.0, 0.0, 0.0), vertices=8)) + elif object_id == "probe.science.boom": + for index in range(4): + z = 0.60 + index * 0.30 + details.append(detail_torus(bpy, f"{master.name}.detail.guide.{index:02d}", (0.0, 0.0, z), 0.042, 0.008, minor_segments=10)) + return details + + + def build_geometry(bpy, materials, step_id): """Build every object of the cumulative step, in STEP_OBJECTS order. @@ -674,6 +797,12 @@ def build_geometry(bpy, materials, step_id): else: fail("unknown object id: " + object_id, 4) + # Secondary engineering detail is generated per contract object and + # merged into the master mesh, keeping the GLB object contract intact. + for object_id in list(STEP_OBJECTS[step_id]): + master = objects[object_id] + merge_details(bpy, master, build_details(bpy, object_id, master, materials)) + ordered = [objects[object_id] for object_id in STEP_OBJECTS[step_id]] for obj, object_id in zip(ordered, STEP_OBJECTS[step_id]): if obj.get("object_id") != object_id: @@ -702,14 +831,21 @@ def add_render_setup(bpy): camera.rotation_euler = (Vector((0.0, 2.0, 0.2)) - camera.location).to_track_quat("-Z", "Y").to_euler() scene.camera = camera - light_data = bpy.data.lights.new("probe.key", type="AREA") - light_data.energy = 900 - light_data.shape = "DISK" - light_data.size = 5.0 - light = bpy.data.objects.new("probe.key", light_data) - bpy.context.collection.objects.link(light) - light.location = (4.0, -4.0, 8.0) - light.rotation_euler = (Vector((0.0, 2.0, 0.2)) - light.location).to_track_quat("-Z", "Y").to_euler() + def add_light(name, location, energy, size, color=(1.0, 1.0, 1.0)): + light_data = bpy.data.lights.new(name, type="AREA") + light_data.energy = energy + light_data.shape = "DISK" + light_data.size = size + light_data.color = color + light = bpy.data.objects.new(name, light_data) + bpy.context.collection.objects.link(light) + light.location = location + light.rotation_euler = (Vector((0.0, 2.0, 0.2)) - Vector(location)).to_track_quat("-Z", "Y").to_euler() + return light + + add_light("probe.key", (4.0, -4.0, 8.0), 900, 5.0, (1.0, 0.98, 0.94)) + add_light("probe.fill", (-6.0, -8.0, 3.0), 300, 8.0, (0.72, 0.82, 1.0)) + add_light("probe.rim", (-5.0, 7.0, -2.5), 650, 3.0, (1.0, 0.86, 0.62)) def export_glb(bpy, objects, output): diff --git a/stage/blender/ship.py b/stage/blender/ship.py index 21e2ab0..e1dd30c 100644 --- a/stage/blender/ship.py +++ b/stage/blender/ship.py @@ -388,6 +388,184 @@ def add_dish(bpy, name, base_x, center_y, center_z, radius, depth, clay_material return obj + + +def merge_details(bpy, master, extras): + """Join detail meshes into the master object so the exported GLB keeps + exactly the contract object names while carrying real secondary detail.""" + if not extras: + return master + for obj in bpy.context.selected_objects: + obj.select_set(False) + for extra in extras: + extra.select_set(True) + master.select_set(True) + bpy.context.view_layer.objects.active = master + bpy.ops.object.join() + return master + + +def detail_torus(bpy, name, location, major_radius, minor_radius, rotation=(0.0, 0.0, 0.0), minor_segments=10): + bpy.ops.mesh.primitive_torus_add( + major_radius=major_radius, + minor_radius=minor_radius, + major_segments=48, + minor_segments=minor_segments, + location=location, + rotation=rotation, + ) + obj = bpy.context.object + obj.name = name + return obj + + +def detail_cube(bpy, name, location, dimensions, rotation=(0.0, 0.0, 0.0)): + bpy.ops.mesh.primitive_cube_add(location=location, rotation=rotation) + obj = bpy.context.object + obj.name = name + obj.dimensions = dimensions + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + return obj + + +def detail_cylinder(bpy, name, location, radius, depth, rotation=(0.0, 0.0, 0.0), vertices=20): + bpy.ops.mesh.primitive_cylinder_add( + vertices=vertices, + radius=radius, + depth=depth, + location=location, + rotation=rotation, + ) + obj = bpy.context.object + obj.name = name + return obj + + +def build_details(bpy, object_id, master, materials): + """Secondary engineering detail per contract object (joined back into the + master mesh before export). Names below never reach the GLB as objects.""" + clay = None + details = [] + + if object_id == "starship.keel.spine": + # Reinforcement rings along the spine + twin conduit lines + equipment + # blocks riding the top ridge. + for index in range(14): + x = -5.2 + index * 0.72 + details.append(detail_torus( + bpy, + f"{master.name}.detail.ring.{index:02d}", + (x, 0.0, 0.0), + 0.40, + 0.028, + rotation=(0.0, math.pi / 2.0, 0.0), + )) + details.append(detail_cylinder(bpy, master.name + ".detail.conduit.0", (-0.8, 0.30, 0.10), 0.035, 9.4, rotation=(0.0, math.pi / 2.0, 0.0))) + details.append(detail_cylinder(bpy, master.name + ".detail.conduit.1", (-0.8, -0.30, 0.10), 0.035, 9.4, rotation=(0.0, math.pi / 2.0, 0.0))) + for index, x in enumerate((-3.9, -1.9, 0.6, 2.9)): + details.append(detail_cube(bpy, f"{master.name}.detail.box.{index:02d}", (x, 0.0, 0.30), (0.5, 0.30, 0.16))) + elif object_id == "starship.hab.torus": + # Outer panel ribs every 15 degrees, inner twin conduits, and two + # comm masts riding the ring. + for index in range(24): + angle = 2.0 * math.pi * index / 24.0 + y = HAB_RADIUS * math.cos(angle) + z = HAB_RADIUS * math.sin(angle) + rotation = (angle, 0.0, 0.0) + details.append(detail_cube( + bpy, + f"{master.name}.detail.rib.{index:02d}", + (HAB_X + 0.06, y, z), + (0.10, 0.08, 0.09), + rotation=rotation, + )) + for offset in (-0.10, 0.10): + details.append(detail_torus( + bpy, + f"{master.name}.detail.conduit.{offset:+.2f}", + (HAB_X + offset, 0.0, 0.0), + HAB_RADIUS - HAB_MINOR - 0.04, + 0.020, + rotation=(0.0, math.pi / 2.0, 0.0), + minor_segments=8, + )) + details.append(detail_cylinder(bpy, master.name + ".detail.mast.0", (HAB_X - 0.02, HAB_RADIUS + HAB_MINOR, 0.0), 0.02, 0.5, rotation=(0.0, math.pi / 2.0, 0.0))) + details.append(detail_cylinder(bpy, master.name + ".detail.mast.1", (HAB_X + 0.02, 0.0, -(HAB_RADIUS + HAB_MINOR)), 0.02, 0.5)) + elif object_id == "starship.hub.core": + for offset, radius in ((-0.74, 0.40), (0.74, 0.40)): + details.append(detail_cylinder(bpy, f"{master.name}.detail.flange.{offset:+.2f}", (HAB_X + offset, 0.0, 0.0), radius, 0.06, rotation=(0.0, math.pi / 2.0, 0.0))) + details.append(detail_torus(bpy, master.name + ".detail.bearing", (HAB_X, 0.0, 0.0), 0.34, 0.035, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=10)) + elif object_id == "starship.core.pressure": + for index in range(4): + x = -4.05 + index * 0.42 + details.append(detail_torus(bpy, f"{master.name}.detail.band.{index:02d}", (x, 0.0, 0.0), 0.58, 0.030, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=10)) + for offset in (-0.92, 0.92): + details.append(detail_cylinder(bpy, f"{master.name}.detail.flange.{offset:+.2f}", (-3.4 + offset, 0.0, 0.0), 0.60, 0.07, rotation=(0.0, math.pi / 2.0, 0.0))) + elif object_id == "starship.shadow.collar": + for index in range(8): + angle = 2.0 * math.pi * index / 8.0 + y = 0.86 * math.cos(angle) + z = 0.86 * math.sin(angle) + details.append(detail_cube(bpy, f"{master.name}.detail.rib.{index:02d}", (-1.35, y, z), (0.34, 0.10, 0.10), rotation=(angle, 0.0, 0.0))) + elif object_id == "starship.storm.cellar": + details.append(detail_cube(bpy, master.name + ".detail.doorframe", (-0.55, 0.0, 0.30), (0.66, 0.52, 0.05))) + details.append(detail_cylinder(bpy, master.name + ".detail.hinge", (-0.55, -0.32, 0.0), 0.035, 0.5)) + elif object_id == "starship.nozzle.magnetic": + # Coil grooves down the bell + coolant lines hugging the cone. + for index in range(5): + fraction = 0.25 + index * 0.14 + x = -6.35 + 2.2 * fraction + radius = 1.15 - 0.55 * fraction + details.append(detail_torus(bpy, f"{master.name}.detail.coil.{index:02d}", (x, 0.0, 0.0), radius, 0.034, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=10)) + for index in range(4): + angle = 2.0 * math.pi * index / 4.0 + math.pi / 4.0 + y = 0.95 * math.cos(angle) + z = 0.95 * math.sin(angle) + details.append(detail_cylinder(bpy, f"{master.name}.detail.coolant.{index:02d}", (-5.35, y, z), 0.028, 2.5, rotation=(0.0, math.pi / 2.0, 0.0))) + elif object_id == "starship.power.truss": + for index in range(6): + t = index / 5.0 + x = -1.2 + t * 5.1 + details.append(detail_cylinder(bpy, f"{master.name}.detail.diagonal.{index:02d}", (x, 0.0, 0.34), 0.022, 0.34, rotation=(0.0, math.pi / 2.0, 0.0), vertices=8)) + details.append(detail_torus(bpy, master.name + ".detail.hoop", (1.6, 0.0, 0.34), 0.40, 0.028, rotation=(math.pi / 2.0, 0.0, 0.0), minor_segments=10)) + elif object_id.startswith("starship.radiator."): + index = int(object_id.rsplit(".", 1)[-1]) + angle = 2.0 * math.pi * index / 6.0 + math.pi / 6.0 + y = 0.95 * math.cos(angle) + z = 0.95 * math.sin(angle) + details.append(detail_cube(bpy, master.name + ".detail.backbone", (-3.6, y, z), (1.5, 0.05, 0.06))) + for rib in (-0.45, 0.0, 0.45): + details.append(detail_cube(bpy, f"{master.name}.detail.stiff.{rib:+.2f}", (-3.6 + rib, y, z), (0.05, 0.06, 0.40))) + elif object_id.startswith("starship.cargo.pod."): + index = int(object_id.rsplit(".", 1)[-1]) + y = (-0.62, 0.62)[index] + for offset in (-0.5, 0.0, 0.5): + details.append(detail_cube(bpy, f"{master.name}.detail.rib.{offset:+.2f}", (0.35 + offset, y, 0.0), (0.05, 0.26, 0.32))) + details.append(detail_cylinder(bpy, master.name + ".detail.endcap", (0.97, y, 0.0), 0.10, 0.05, rotation=(0.0, math.pi / 2.0, 0.0))) + elif object_id == "starship.nose.cone": + details.append(detail_torus(bpy, master.name + ".detail.seam.0", (4.45, 0.0, 0.0), 0.26, 0.02, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=8)) + details.append(detail_torus(bpy, master.name + ".detail.seam.1", (4.75, 0.0, 0.0), 0.16, 0.018, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=8)) + elif object_id == "starship.comm.dish": + # Mounting mast rising from the keel deck + gimbal block under the + # dish base, so the dish reads as bolted hardware instead of floating. + details.append(detail_cylinder(bpy, master.name + ".detail.post", (3.40, 0.0, 0.37), 0.045, 0.30, vertices=12)) + details.append(detail_cube(bpy, master.name + ".detail.gimbal", (3.40, 0.0, 0.46), (0.16, 0.16, 0.10))) + details.append(detail_cube(bpy, master.name + ".detail.baseplate", (3.40, 0.0, 0.24), (0.26, 0.26, 0.05))) + elif object_id.startswith("starship.spoke."): + index = int(object_id.rsplit(".", 1)[-1]) + angle = 2.0 * math.pi * index / SPOKE_COUNT + end_y = HAB_RADIUS * math.cos(angle) + end_z = HAB_RADIUS * math.sin(angle) + # Collar at the ring end and a hinge ring at the hub end. + details.append(detail_torus(bpy, master.name + ".detail.cuff", (HAB_X, end_y * 0.92, end_z * 0.92), 0.16, 0.045, rotation=(angle, 0.0, 0.0), minor_segments=12)) + details.append(detail_torus(bpy, master.name + ".detail.root", (HAB_X + 0.06, 0.0, 0.0), 0.40, 0.045, rotation=(0.0, math.pi / 2.0, 0.0), minor_segments=12)) + else: + return [] + + _ = clay + return details + + # --------------------------------------------------------------------------- # Starship layout (units are display metres, unscaled示意 dimensions). # @@ -482,6 +660,12 @@ def build_geometry(bpy, materials, step_id): else: fail("unknown object id: " + object_id, 4) + # Secondary engineering detail is generated per contract object and + # merged into the master mesh, keeping the GLB object contract intact. + for object_id in list(STEP_OBJECTS[step_id]): + master = objects[object_id] + merge_details(bpy, master, build_details(bpy, object_id, master, materials)) + ordered = [objects[object_id] for object_id in STEP_OBJECTS[step_id]] for obj, object_id in zip(ordered, STEP_OBJECTS[step_id]): if obj.get("object_id") != object_id: @@ -510,14 +694,21 @@ def add_render_setup(bpy): camera.rotation_euler = (Vector((-0.5, 0.0, 0.0)) - camera.location).to_track_quat("-Z", "Y").to_euler() scene.camera = camera - light_data = bpy.data.lights.new("ship.key", type="AREA") - light_data.energy = 1400 - light_data.shape = "DISK" - light_data.size = 6.0 - light = bpy.data.objects.new("ship.key", light_data) - bpy.context.collection.objects.link(light) - light.location = (5.0, -6.0, 9.0) - light.rotation_euler = (Vector((-0.5, 0.0, 0.0)) - light.location).to_track_quat("-Z", "Y").to_euler() + def add_light(name, location, energy, size, color=(1.0, 1.0, 1.0)): + light_data = bpy.data.lights.new(name, type="AREA") + light_data.energy = energy + light_data.shape = "DISK" + light_data.size = size + light_data.color = color + light = bpy.data.objects.new(name, light_data) + bpy.context.collection.objects.link(light) + light.location = location + light.rotation_euler = (Vector((-0.5, 0.0, 0.0)) - Vector(location)).to_track_quat("-Z", "Y").to_euler() + return light + + add_light("ship.key", (5.0, -6.0, 9.0), 1400, 6.0, (1.0, 0.98, 0.94)) + add_light("ship.fill", (-4.0, -9.0, 2.0), 420, 8.0, (0.72, 0.82, 1.0)) + add_light("ship.rim", (-7.5, 5.0, -3.0), 900, 3.0, (1.0, 0.86, 0.62)) def export_glb(bpy, objects, output): |
