# Tender docking carousel (three arms), game-ready A ceiling-hung turret with three booms 120° apart. Each boom 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. 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 three 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 | ## 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 booms) | 1 | hub axis | yaw = carousel angle | | `SM_DockArm_UpperLink` (both side plates) | 3 | shoulder axis at the boom tip (6.0007, 0, 0.94 in the boom frame) | 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: `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. - In the .blend the empties `Arm1..3` have two custom properties that drive this: `lower_m` (0..5.3) and `claw_open` (0..1). ## 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. 7 unique meshes with the hose, 27.0k triangles; 60.1k for the whole assembly (26 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 492, turret 393, upper link 322, lower link 384, head 401, 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 → up beside the boom root → along 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. Along boom and links it rides rigidly; at shoulder, elbow and wrist it is carried by fan bones (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, so the hose follows `lower_m`. **Rigged model:** the fan bones are part of `DockArm_Rig` (`ArmN_HoseS1..8`, `_HoseE1..9`, `_HoseW1..4`, hidden bone collection). - **Added to the parts:** port stub on the pod, clips and saddles on the boom, a bar at the tip, three stand-offs on the upper link, three hangers on the lower link, flange 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 (23 part meshes with one vertex group each, plus the three hoses). `fbx/SK_DockArm_Carousel.fbx` is the skeletal export (86 bones: 23 joints and 63 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_Upper` | shoulder, at the boom tip | 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 (shoulder -2..92°, elbow -2..180°, wrist ±100°). Hoist: Upper +phi, Lower +2·phi, Head +phi, with `phi = asin(lowered / 6.0)`. - **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..5.3 m and unfolds the arm with the head level; the ring beside it, `CTRL_ArmN_Claws`, turns 0..25° and opens the four claws. Set the square's custom property `auto` to 0 to pose 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). Six hidden helper bones (`Mechanism`) carry phi and 2·phi from drivers and the joints copy them, so the motion is exact. The drivers are simple expressions (no auto-run needed). - Checked: with the joints set by hand and with the handles, every part lands within 0.006 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.001 mm with the head level; the saved file re-opened and posed; the FBX re-imported (86 bones, parents, positions, 26 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); UV layers, the baked maps and all materials are in `build\surfaces.py`. All three are run by `build_dockarm.py` (about two minutes, most of it the bake on the GPU). ## 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 three pitches per arm instead (formula above). The clearance obstacle list there would also get the booms. - 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).