# Tender docking carousel (three arms), game-ready A ceiling-hung turret with three booms 120° apart. Each boom hangs on a pitch joint 1.5 m from the hub (the boom joint, added 2026-10-16) and carries a folding two-link hoist (shoulder, elbow, wrist, built like an industrial robot arm) and a clamp head with four claws that closes on the Tender's hatch collar. To BOARD the hoist lets the Tender down; on down to POOL the boom joint pitches the whole arm into the moon pool. It is the modelled version of the carousel blockout in the Unreal port (`Vehicles/CarrierCarousel.cpp`, DESIGN_UE.md 3.5). **It is not in the Unreal project yet.** ## Files - `DockArm_Carousel.blend` (Blender 5.2): the assembly in the stowed pose. `DockArm_Parts` (the shared meshes at their own origin) and `DockArm_Collision` (UCX hulls) are excluded from the view layer; `Studio` is camera and lights only. - `fbx/SM_DockArm_.fbx`: one file per unique mesh, at its own pivot, with its `UCX_` hulls. `fbx/DockArm_Carousel_Assembled.fbx`: the whole thing stowed, for a quick look. - `dockarm_layout.json`: pivots, parents and the hoist formula in Unreal units. `textures/T_DockArm_Decals_BaseColor.png`: decal atlas (arm numbers, hazard chevrons, generic labels; no brand names). `previews/`: renders. `build/`: the scripts. - Ore hose (added 2026-10-13): mesh `SM_DockArm_OreHose` in the .blend, `fbx/SK_DockArm_OreHose.fbx` (one arm's hose with its bones), `textures/T_DockArm_Duct_BaseColor.png` (opacity in its alpha), `_Roughness.png`, `_Normal.png`. See "Ore hose" below. - Surfaces (2026-10-13): `textures/T_DockArm_Masks.png` and `T_DockArm_BevelNormal.png` (4096 px, baked from the model). The tiling sets are the Season 2 pack's own files, read in place from `../vessel-interior/textures/ModSci_Engineer/Textures`. **Textures are referenced by relative path, not packed**: keep the .blend files in this folder. See "Surfaces" below. ## What comes from the Unreal blockout | Blockout | Model | |---|---| | hub at `ArmsRoot`, bay ceiling at z 1.20 | origin = hub axis at carrier z 0; the mount flange sits on z 1.20, nothing is higher | | three arms 120° apart, cradle radius 6.0 | three booms; tips at radius 6.0007, turned 0.86° so the head is centred on the hatch collar (9 cm ahead of the Tender origin) | | arm 1 over the pool at hub angle 0 (Unreal +Y) | `Arm1` points to Blender −Y | | Tender origin z −1.0 STOWED, −6.3 BOARD | links lie flat beside the boom at STOWED; `lower_m` 5.3 is BOARD | | clamp plate on the hatch collar | head with landing feet on the collar flange and four claws on the collar wall | | carriage moved in z, mast scaled | **changed:** a two-link knee hoist. The blockout leaves 9 cm between beam and clamp when stowed and 0.64 m under the ceiling, so a rigid mast cannot exist there | | (nothing: BOARD is the lowest stop) | **added 2026-10-16:** a boom joint 1.5 m from the hub; below BOARD it pitches the arm down and puts the Tender into the moon pool (POOL, 4 m lower). See "The boom joint and the pool" | ## Scale, axes, pivots - 1 unit = 1 m, Z up. Unreal = (x, −y, z) × 100 cm. Span 13.4 m across the heads, 1.0 m tall stowed, reaches 5.3 m down. - Moving parts are separate objects with the origin on their axis, scale 1: | Mesh | Count | Pivot | Motion | |---|---|---|---| | `SM_DockArm_Mount` | 1 | hub axis | fixed to the carrier | | `SM_DockArm_Turret` (drum + three root stubs with the joint clevises) | 1 | hub axis | yaw = carousel angle | | `SM_DockArm_Boom` | 3 | the boom joint's pin, (1.50, 0, 0.85) in the boom frame | pitch, 0 down to BOARD, then down to 51° (POOL on the launch arm) | | `SM_DockArm_UpperLink` (both side plates) | 3 | shoulder axis at the boom tip, (4.5007, 0, 0.09) from the pin ((6.0007, 0, 0.94) in the boom frame when the boom is level) | pitch | | `SM_DockArm_LowerLink` (both plates, elbow servos) | 3 | elbow axis, 3.0 m from the shoulder | pitch | | `SM_DockArm_Head` | 3 | wrist axis, 3.0 m from the elbow, 1.94 m above the Tender origin | pitch (stays level) | | `SM_DockArm_Claw` | 12 | its hinge under the head rim, at 45/135/225/315° | swings out 25° to open | - Hoist to BOARD: `phi = asin((StowedZ − TenderOriginZ) / 6.0)`. In Blender: upper link Y rotation −phi, lower +2·phi, head −phi (Unreal pitch +phi, −2·phi, +phi). 0 at STOWED, 62.05° at BOARD. The wrist stays exactly under the shoulder (with `reach_in` it moves in toward the hub by `r (y / 5.3)²`). Below BOARD the boom joint takes over: next section. - In the .blend the empties `Arm1..3` have three custom properties that drive this: `lower_m` (0..5.3 BOARD..9.3 POOL), `reach_in` (0..2.5; the ship uses 2.34 on the launch arm) and `claw_open` (0..1). `ArmN_Mech1..6` are tiny empties whose location channels hold the mechanism's staged values (drivers, all simple expressions): leave them be. ## The boom joint and the pool (2026-10-16) The user, with three marked-up pictures of the ship (a circle on the boom near the hub, the boom drawn swung down, arrows from the Tender into the moon pool): *"there need be another joint as shown as red circle line to be able to put down subtub tenders into moon pool. the joint needed to be added three of the arms accordingly ... the current tender position is ok. This is for when user get in and wants to exit the ship so there is an order. after user gets in arms bent from the new joint that you are making and puts tender inside moon pool. so this needs re-rigging too"*. - **The joint:** each boom now hangs on a pin 1.50 m from the hub axis, 0.85 m up (mid-height of the root). The root stub with the clevis (two 9 cm plates rounded about the pin, a pitch servo on each cheek, the servo pack with the arm number under it) stays part of the turret; the boom proper is a part of its own, `SM_DockArm_Boom`, with its eye on the pin, the beam, a junction box for the keel hoses and the shoulder axle housing at the tip. - **The motion, one handle:** `lower_m` 0..5.3 is the hoist of before (BOARD unchanged: the ship's launch arm keeps 2.34 m of reach, 51.1 / 149.9 / 98.8 degrees). From 5.3 to 9.3 (POOL) the head goes straight on down with its reach kept, the knee keeps the bend it had at BOARD, and the boom pitches just as far as the links need: the shoulder is the upper intersection of the circle of radius 4.50 about the pin with the circle of radius D (the knee's bend) about the wrist. The head stays level throughout (wrist = elbow − shoulder + boom). The exact formulas, with the degenerate case (r near 0, deep: the knee opens and the boom points at the wrist), are in `dockarm_layout.json` under `hoist`, and in `build/kinematics.py`, which is also what both .blend files run as drivers. - **At POOL on the ship's launch arm** (reach 2.34): boom 51.4°, shoulder 119.2°, elbow 149.9°, wrist 82.0°. The Tender's origin is 10.3 m below the hub: in the ship its top is 1.3 m below the deck and its keel 3.2 m above the hull's aperture. With no reach (r 0) the same depth needs 58.7° and 154.9° (the arm nearly straight); 4.0 m below BOARD is as deep as the links can go at r 0, so POOL is 4.0 for every reach. - **Ranges:** boom 0..75°, shoulder 0..172°, elbow 0..180°, wrist ±105°. - **The hose** passes through the joint's axis line beside the clevis (at y −0.72, outside the pitch servo) and bends there on six fan bones like at the other joints. Its clips moved onto the boom; the coupling on the head moved 10 cm out and is a ring now (the build's own check found a ring of the wrist fan grazing it at the ship's BOARD pose). - **Checked:** no two parts intersect and the hose touches nothing in 23 poses (0..5.3 m with the claws closed and open; 6.3, 7.3, 8.3, 9.3 m at reach 2.34; the corners 7.3 / 8.8 / 9.3 m at reach 0, 9.3 at 2.5, 8.0 at 1.2); the drivers put the wrist within 1 mm of the formulas and keep the head level in every pose; against the real Kazah through the whole descent the gaps are unchanged (17 mm feet, 20 mm claw pads, 0.27 m links, 0.44 m hose); the rig agrees with the unrigged model to 0.01 mm and with the Python reference to 0.05°. - **Not changed:** stowed and BOARD (the ship's launch arm is exactly where it was), the Kazah fit, the surfaces. ## Mesh notes - No booleans. Quads plus triangle fans on the ends of turned parts and a few bevel corners; no n-gons, no open edges, no zero-area faces, outward normals. 8 unique meshes with the hose, 29.0k triangles; 66.7k for the whole assembly (29 mesh objects). The hose is an all-quad tube, open at its two ends (they sit inside the port and the coupling). - Two UV layers on every part and 11 materials: see "Surfaces". - Collision: `UCX__NN` convex boxes, coarse (the blockout has no collision on the carousel at all). ## Checked - No two parts intersect in 12 poses (0, 0.25, 1.0, 2.5, 4.0, 5.3 m lowered × claws closed/open), all three arms present. - Against the real Kazah mesh (`models/Kazah-stern-cavity-fixedv4.blend`, linked only for the check and the renders): no crossing triangles; smallest gaps 17 mm (landing feet over the flange) and 20 mm (claw pads to the collar wall); links stay 0.24 m or more from the hull; the canopy opened 55° clears the arm at BOARD. - Every FBX re-imported and compared with the .blend (bounds, triangle counts, materials, hulls, placement): `build/verify_fbx.py`. ## Surfaces (2026-10-13: "do it more realistic with good textures") The first version had one flat colour per material and long glowing lines. Now every hard surface is a tiling texture set from the user's own Modular SciFi Season 2 pack (the sets the vessel interior uses, already in the Unreal project) under a tint, plus two maps baked from this model. Nothing was downloaded. The ship is a clean, grayish-white vessel, so this is a well-kept machine: fine scratches and smudges in the sheen, a little shade in the gaps, a few chips on exposed edges, no rust. **UV layers** (the order matters: Unreal builds its tangents from the first) - `UVMap` (UV0): a unique unwrap inside the part's square of one shared 4096 px atlas, for the two baked maps. The turret is three equal sectors and the mount six; one sector is unwrapped and the others use the same texels one whole tile further along U (samplers must wrap). Decal quads carry their decal-sheet coordinates here. - `UV_Tile` (UV1): metres. Turned surfaces are unrolled, everything else is box-projected. Tiling sets are sampled at UV1 / repeat. **Baked maps** (Cycles, `build/surfaces.py`) - `T_DockArm_Masks.png`, linear: R ambient occlusion, G edges (1 on a hard edge, 0 from 20 mm away), B crevices (1 open, lower in a gap). - `T_DockArm_BevelNormal.png`: tangent space on UV0, OpenGL (flip green for Unreal). Rounds every hard edge by 12 mm, so edges catch the light without any extra geometry. - Sharpness on the model: mount 326, turret 324, boom 381, upper link 322, lower link 327, head 341, claw 1367 px per metre. **Materials** (sets are under `ModSci_Engineer/Textures/Base/`; tint and ranges are linear values) | Material | Set | Repeat | Tint | Roughness | On top | |---|---|---|---|---|---| | `M_DockArm_Paint` | ScratchedMetal Diff, Rough, Normal | 0.90 m | 0.64, 0.65, 0.65 | 0.34, up to 0.60 in scratches | smudges +0.05, shade in gaps, few chips | | `M_DockArm_Graphite` | StainedSteel Diff; ScratchedMetal Rough, Normal | 0.70 m | 0.040, 0.043, 0.048 | 0.42 to 0.66 | smudges +0.06, few chips | | `M_DockArm_Steel` | StainedSteel Diff, Rough; `Glass/T_Scratch_Norm` | 0.50 m | 0.60, 0.61, 0.63, metallic 1 | 0.22 to 0.36 | smudges +0.05 | | `M_DockArm_SafetyOrange` (claws) | ScratchedMetal Diff, Rough, Normal | 0.60 m | 0.78, 0.19, 0.02 | 0.36 to 0.62 | chips on the edges | | `M_DockArm_Rubber` | ScratchedMetal Rough | 0.40 m | 0.017 | 0.62 to 0.86 | a little dust | | `M_DockArm_HoseBand` | as Steel, on the hose's own UVs | 0.35 m | as Steel | 0.22 to 0.36 | | | `M_DockArm_LensGlass` | smudge mask only | 0.60 m | near-black | 0.16 to 0.24 | lenses under the hub and the head | | `M_DockArm_Decals` | own sheet `T_DockArm_Decals_BaseColor` | | | 0.55 | the scratch map takes bits of the print away | | `M_DockArm_GlowCyan`, `GlowAmber` | | | 0.10, 0.85, 1.0 and 1.0, 0.55, 0.08 | | emission 1.2 (was 6 and 5) | | `M_DockArm_OreHose` | own `T_DockArm_Duct_*` | 0.69 m | | film 0.07 to 0.12, rib 0.46 | see "Ore hose" | - Smudge and dust mask for all of them: `Glass/T_Glass_DirtRough.png`, added to the roughness at a few hundredths. - The exact recipe (how the masks are used, every range, the value range each map really spans) is in `dockarm_layout.json` under `surfaces`, written for whoever rebuilds the materials in Unreal. - **Lights only where they signal something:** the lock ring on each head, the servo rings at elbow and wrist, the claw lock lights, the status bars on the servo packs and the head housing. Gone: the light band on the mount, the ring round the hub, the light lines along the booms and the lower links, the rings on the hose ports. The lenses are dark glass. - Pictures are rendered in Cycles (`build/render_previews.py`); the surroundings the metal mirrors come from Blender's bundled studio light, for the pictures only. ## Ore hose (added 2026-10-13) A clear, spiral-ribbed flexible duct (22 cm) per arm for unloading cargo, with steel clamp bands: the idea of the kit's fabric-coated flexible duct (`EP_SM_Ceiling_CoatedPipe`, the pieces named `Truss_A..` in the vessel interior), but its own mesh and its own textures, so it can bend and be see-through. (The kit's coated-pipe maps are baked for its own bent pieces and cannot be laid along a 14 m hose.) - **Route:** port on the hub's underside pod → under the root stub → through the boom joint's axis line, outside its clevis → up onto the top of the boom → a flat U-turn round the tip → back over the shoulder → down the upper link → U-turn round the elbow → down the lower link → through the wrist → coupling and duct into the housing on the head. - **Beside the arm, not in the middle.** Folded, the boom lies exactly between the link plates and the head sits 4 cm under the boom tip, so a hose on the centre line has nowhere to be. It runs in one plane 0.86 m to the stern side of the arm (away from the Tender's canopy). Nothing of the arm ever enters that plane, so the hose can pass every joint there. - **It bends with the joints.** Every ring of the hose is carried by one bone. From the port to the boom joint it is rigid with the turret, along the boom and the links with those; at the boom joint, shoulder, elbow and wrist it is carried by fan bones (6, 8, 9 and 4 of them) that each take an even share of the joint's angle. Lowered to BOARD the loop round the elbow closes from 180° to 56° (the film bunches up), and the piece over the shoulder stretches by about a third. - **Unrigged model:** each arm has a small armature `ArmN_HoseRig` whose bones copy the arm's objects (the boom since 2026-10-16), so the hose follows `lower_m`. **Rigged model:** the fan bones are part of `DockArm_Rig` (`ArmN_HoseR1..6`, `_HoseS1..8`, `_HoseE1..9`, `_HoseW1..4`, hidden bone collection). - **Added to the parts:** port stub on the pod (turret), two clips beside the climb and three saddles on the boom's top, a bar at the tip (boom), three stand-offs on the upper link, three hangers on the lower link, a coupling ring and duct on the head. - **Material `M_DockArm_OreHose`** (since the surfaces round): clear film with a grey spiral rib every 4.9 cm. The opacity is in the alpha of `T_DockArm_Duct_BaseColor` (film about 0.2 to 0.3, rib 1), so it is more than half see-through; reflections follow the viewing angle (Fresnel), which is what makes clear plastic visible; a little dust from the pack's smudge mask. Two-sided. The normal map is OpenGL (flip green for Unreal). One texture repeat is one circumference (0.69 m) of hose. Clamp bands: `M_DockArm_HoseBand`. - **Checked:** no contact with any part away from its clamps and fittings, and none of the hose with itself, in the 12 poses; 0.46 m or more from the Tender, also with the canopy open; rigged and unrigged hose agree to 0.006 mm. - **Needs headroom.** The hose tops out at z 1.37: above the old blockout bay's ceiling (1.20). In the vessel interior the roof is far above, but the hull closes in to 6.49 m from the hub at that height; the tip U-turn is kept inside 6.39 m, which leaves about 10 cm when an arm points abeam. - **A scene that already holds the rigged carousel** (the vessel interior) takes new meshes and materials in place, without touching the rig, with `build/scene_update_surfaces.py` (`update_surfaces()`); it checks that every part is where it was. ## Rigged version (added 2026-10-12) `DockArm_Carousel_Rigged.blend` is the same model on one armature, `DockArm_Rig`: a bone on every joint, each part skinned to its bone with weight 1 (26 part meshes with one vertex group each, plus the three hoses). `fbx/SK_DockArm_Carousel.fbx` is the skeletal export (107 bones: 26 joints and 81 hose fan bones), `dockarm_rig.json` lists the bones in Unreal units. | Bone | Joint | Turns about | Positive angle | |---|---|---|---| | `Root` | (carries the ceiling mount) | free | | | `Turret` | hub | its local Y (it points up) | counter-clockwise seen from above | | `ArmN_Boom` | the boom joint, 1.5 m from the hub (2026-10-16) | local X | pitches the boom down | | `ArmN_Upper` | shoulder, at the boom tip (a child of `ArmN_Boom`) | local X | lowers the hoist | | `ArmN_Lower` | elbow | local X | lowers the hoist | | `ArmN_Head` | wrist | local X | lowers the hoist (keeps the head level) | | `ArmN_Claw1..4` | claw hinges | local X | opens, 0..25° | - Rest pose = stowed. Every joint bone is locked to its one axis and has a range limit (boom 0..75°, shoulder 0..172°, elbow 0..180°, wrist ±105°). Hoist to BOARD: Upper +phi, Lower +2·phi, Head +phi, with `phi = asin(lowered / 6.0)`. - **On down to POOL** (2026-10-16): above 5.3 m the handle drives the boom joint too; the section "The boom joint and the pool" says how. The vessel interior's `carousel_rigged.add_reach()` must not be run on this rig: it would rewrite the shoulder and elbow drivers without the pool stage. - **Reach in** (taken over from the vessel interior on 2026-10-13, where the launch slot is nearer the hub than the cradle radius): the custom property `reach_in` on `CTRL_ArmN_Hoist` (metres, 0..2.5) lets the head move in toward the hub while it is let down, x = reach_in · (y / 5.3)², and stay level. With D = √(x² + y²): shoulder = atan2(y, x) − acos(D / 6), elbow = π − 2·acos(D / 6), wrist = elbow − shoulder. 0 is the plain zigzag above. The ship uses 2.34 on arm 1: 51.1°, 149.9°, 98.8° at BOARD. - Pose Mode handles (bone collection `Controls`): the square outboard of each head, `CTRL_ArmN_Hoist`, moves down 0..9.3 m: to 5.3 (BOARD) it unfolds the arm with the head level, on down to 9.3 (POOL) the boom joint pitches the arm into the pool; the ring beside it, `CTRL_ArmN_Claws`, turns 0..25° and opens the four claws. The square's custom property `reach_in` is how far the head moves in toward the hub by BOARD (the ship: 2.34 on the launch arm); set its `auto` to 0 to pose boom, shoulder, elbow and wrist by hand. - The hoist is not an IK chain. Stowed, the wrist lies exactly on the shoulder axis, the one case a two-bone IK cannot solve (tried: 55 mm off at rest, metres off with the turret turned). 27 hidden helper bones (`Mechanism`) carry the staged values (as custom properties on `MCH_ArmN_M1..M6`) and the four angles (`MCH_ArmN_Theta`, `_Phi`, `_2Phi`) from drivers, and the joints copy them, so the motion is exact. The drivers are simple expressions (no auto-run needed). Found on the way: drivers of one armature that read each other's bone transforms are evaluated in no reliable order (one arm came out wrong), so the chain reads custom properties instead. - Checked: with the joints set by hand and with the handles, every part lands within 0.01 mm of the unrigged model in 11 poses including a turned turret (the hoses included); `reach_in` 2.34 at BOARD puts the wrist 2.34 m in and 5.3 m down to 0.003 mm with the head level; five pool-stage poses (7.0 and 9.3 m at 2.34, 9.3 at 0, 8.0 at 1.2, 6.0 at 2.5) agree with the unrigged model to 0.01 mm and with the Python reference to 0.01°; the saved file re-opened and posed; the FBX re-imported (107 bones, parents, positions, 29 meshes, weights). `build/verify_rig.py`. - The skeletal FBX holds the deform bones only (no handles), is written in centimetres with no scale on any node, and is turned like the part files. **It has not been imported into Unreal.** ## Rebuild ``` python build\make_decals.py textures blender -b --python build\build_dockarm.py # model, .blend, layout json, fbx blender -b --python build\verify_fbx.py blender -b --python build\render_previews.py # fit check against the Kazah + previews blender -b --python build\rig_dockarm.py # rigged .blend, rig json, skeletal fbx (reads the model file) blender -b --python build\verify_rig.py blender -b --python build\render_rig_preview.py python build\compose_rig_preview.py previews ``` The hose lives in `build\hose.py` (path, brackets, mesh) and `build\hose_rig_setup.py` (its bones); the arm's motion (the boom joint, the hoist, the pool stage: a Python reference and the driver chain both .blend files use) in `build\kinematics.py`; UV layers, the baked maps and all materials in `build\surfaces.py`. All of them are run by `build_dockarm.py` (about two minutes, most of it the bake on the GPU). A scene that holds the older rigged carousel takes this one in place with `build\scene_update_rig.py` (`update_rig()`, and `check_descent()` runs the launch arm into the pool against the scene). ## Open - The ore hose is empty: no ore moves in it yet. In Unreal its film needs a translucent material, and the hose is either the skeletal mesh (bones driven as in `dockarm_layout.json` / `dockarm_rig.json`) or a spline mesh laid along the same path. - In the vessel interior the hose clears the hull by about 10 cm at its top when an arm points abeam. - Putting it into Unreal: `CarrierCarousel.cpp` drives one carriage z and a mast scale per arm; with this model it sets four pitches per arm instead (boom, shoulder, elbow, wrist: `dockarm_layout.json` `hoist`), and BOARD is no longer the lowest stop: the launch arm goes on to POOL. The clearance obstacle list there would also get the booms. - The ore hose stretches where it wraps a joint: at the shoulder by about half at 119° (POOL) and nearly double at 155° (POOL with no reach). A skinned film cannot slide; a real hose would. - No LODs. The materials exist in Blender only: in Unreal they have to be rebuilt from the table above and `dockarm_layout.json` (`surfaces`); the FBX files carry both UV layers and the material names. Per-part FBX use the project's export pair (forward −Y, up Z, data turned −90° about Z; import with "convert scene" + "force front X axis", as `Tools/ue/assets_12_arm.py` does).