diff --git a/README.md b/README.md
index f8d833a..bc67903 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Requires **Blender 5.2 LTS** or newer (files were written with 5.2.3).
| Folder | What is in it |
|---|---|
-| [`instruments/`](instruments/) | One `.blend` per ARM instrument (AERI, KAZR, DL, MPL, MWR3C, MET tower, ECOR, SEBS, SIRS, RWP, …) plus the C-SAPR2 radar and the KAWG AWOS |
+| [`instruments/`](instruments/) | One `.blend` per ARM instrument (AERI, KAZR, DL, MPL, MWR3C, MET tower, ECOR, SEBS, SIRS, RWP, …) plus the C-SAPR2 radar, the KAWG AWOS and the radiosonde balloon launch (balloon, sonde, line and launch crew, animated) |
| [`containers/`](containers/) | AMF2 instrument containers, AOS platform, office shelter, and the SACR container (M1 and S2 versions) |
| [`buildings/`](buildings/) | Farmstead (M1), MIRF buildings and center pivots (S1), KAWG hangars and terminal (S2), Louisa power station, flagpole, site signs |
| [`vehicles/`](vehicles/) | Farm vehicles and implements, and the ARM Challenger 850 research aircraft |
diff --git a/instruments/BALLOON/BALLOON.blend b/instruments/BALLOON/BALLOON.blend
new file mode 100644
index 0000000..9104dd2
Binary files /dev/null and b/instruments/BALLOON/BALLOON.blend differ
diff --git a/instruments/README.md b/instruments/README.md
index c6a9d25..ab96d8c 100644
--- a/instruments/README.md
+++ b/instruments/README.md
@@ -2,18 +2,21 @@
One file per instrument; link the `INST_*` (or `CSAPR2`) collection. Root at the origin.
+`INST_BALLOON` is the radiosonde launch at M1: balloon, sonde, the line (a stretch-to constraint from sonde to balloon) and the two-person launch crew (`BALLOON_Crew`). It carries the release animation over frames 1–480; the *_Hold/*_Release crew poses swap visibility at release.
+
| Preview | Collection | File |
|---|---|---|
|
| `INST_AERI` | [`AERI/AERI.blend`](AERI/AERI.blend) (0.2 MB) |
|
| `INST_AOSMET` | [`AOSMET/AOSMET.blend`](AOSMET/AOSMET.blend) (0.1 MB) |
|
| `INST_ASI` | [`ASI/ASI.blend`](ASI/ASI.blend) (0.2 MB) |
|
| `INST_AWOS` | [`AWOS/AWOS.blend`](AWOS/AWOS.blend) (0.3 MB) |
+|
| `INST_BALLOON` | [`BALLOON/BALLOON.blend`](BALLOON/BALLOON.blend) (1.9 MB) |
|
| `INST_CEIL` | [`CEIL/CEIL.blend`](CEIL/CEIL.blend) (0.1 MB) |
|
| `CSAPR2` | [`CSAPR2/CSAPR2.blend`](CSAPR2/CSAPR2.blend) (1.2 MB) |
|
| `INST_CSPHOT` | [`CSPHOT/CSPHOT.blend`](CSPHOT/CSPHOT.blend) (0.2 MB) |
|
| `INST_DL` | [`DL/DL.blend`](DL/DL.blend) (0.2 MB) |
-|
| `INST_ECOR_4m` | [`ECOR/ECOR.blend`](ECOR/ECOR.blend) (0.3 MB) |
|
| `INST_ECOR` | [`ECOR/ECOR.blend`](ECOR/ECOR.blend) (0.3 MB) |
+|
| `INST_ECOR_4m` | [`ECOR/ECOR.blend`](ECOR/ECOR.blend) (0.3 MB) |
|
| `INST_IRT` | [`IRT/IRT.blend`](IRT/IRT.blend) (0.1 MB) |
|
| `INST_KAZR` | [`KAZR/KAZR.blend`](KAZR/KAZR.blend) (0.2 MB) |
|
| `INST_LDIS` | [`LDIS/LDIS.blend`](LDIS/LDIS.blend) (0.1 MB) |
diff --git a/instruments/preview/BALLOON.jpg b/instruments/preview/BALLOON.jpg
new file mode 100644
index 0000000..1ff58d4
Binary files /dev/null and b/instruments/preview/BALLOON.jpg differ
diff --git a/scenes/M1/M1.blend b/scenes/M1/M1.blend
index e34d96c..de211c5 100644
Binary files a/scenes/M1/M1.blend and b/scenes/M1/M1.blend differ
diff --git a/scenes/README.md b/scenes/README.md
index 9112867..e75dca0 100644
--- a/scenes/README.md
+++ b/scenes/README.md
@@ -4,7 +4,7 @@ Each site scene is a thin file holding cameras, lights, world and render setting
## M1
-`M1/M1.blend` (2.3 MB) — layers: `M1/layers/m1_amf2_site.blend` (0.2 MB), `M1/layers/m1_fields.blend` (11.8 MB), `M1/layers/m1_ground.blend` (45.4 MB)
+`M1/M1.blend` (0.5 MB) — layers: `M1/layers/m1_amf2_site.blend` (0.2 MB), `M1/layers/m1_fields.blend` (11.8 MB), `M1/layers/m1_ground.blend` (45.4 MB)
diff --git a/tools/balloon_track.py b/tools/balloon_track.py
new file mode 100644
index 0000000..0ad23d4
--- /dev/null
+++ b/tools/balloon_track.py
@@ -0,0 +1,15 @@
+"""Print world positions of the balloon-launch parts over the animation (for before/after checks)."""
+import bpy, json, sys
+out = sys.argv[sys.argv.index('--') + 1]
+sc = bpy.context.scene
+names = ('Balloon', 'Sonde', 'BalloonLine', 'Launcher_Hold', 'Launcher_Release', 'Helper_Hold', 'Helper_Release')
+res = {}
+for f in (1, 60, 120, 180, 240, 300, 360, 480):
+ sc.frame_set(f)
+ dg = bpy.context.evaluated_depsgraph_get()
+ for inst in dg.object_instances:
+ n = inst.object.name.split('.')[0]
+ if n in names and inst.show_self:
+ m = inst.matrix_world
+ res[f'{f}:{n}'] = [round(v, 3) for v in list(m.translation) + list(m.col[2][:3])]
+json.dump(res, open(out, 'w'), indent=0, sort_keys=True)
diff --git a/tools/build_balloon.py b/tools/build_balloon.py
new file mode 100644
index 0000000..4d7c52e
--- /dev/null
+++ b/tools/build_balloon.py
@@ -0,0 +1,38 @@
+"""Move the M1 radiosonde balloon launch (balloon, sonde, line and launch crew) out of
+scenes/M1/M1.blend into instruments/BALLOON/BALLOON.blend and link it back.
+blender -b scenes/M1/M1.blend --python tools/build_balloon.py
+
+All parts are parented to a new root at the launch point (transforms and the
+release animation preserved), the root is moved to the origin in the library,
+and M1 gets a collection instance at the original launch point.
+"""
+import bpy, os, sys
+from mathutils import Matrix, Vector
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import ablib as A
+D = bpy.data
+c = D.collections['BalloonLaunch']
+c.name = 'INST_BALLOON'
+crew = D.collections['BalloonCrew']
+crew.name = 'BALLOON_Crew'
+
+sonde = D.objects['Sonde']
+root = D.objects.new('BALLOON_root', None)
+root.empty_display_size = 1.0
+c.objects.link(root)
+root.matrix_world = Matrix.Translation(Vector((sonde.matrix_world.translation.x,
+ sonde.matrix_world.translation.y, -0.08)))
+bpy.context.view_layer.update()
+inv = root.matrix_world.inverted()
+for o in list(c.all_objects):
+ if o is root or o.parent is not None:
+ continue
+ o.parent = root
+ o.matrix_parent_inverse = inv # keeps world placement and the keyframed motion
+bpy.context.view_layer.update()
+
+A.extract_normalized('instruments/BALLOON/BALLOON.blend', [('INST_BALLOON', 'BALLOON_root')])
+A.purge()
+bpy.ops.file.make_paths_relative()
+bpy.ops.wm.save_mainfile(compress=True, relative_remap=True)
+print('BALLOON_DONE')
diff --git a/tools/preview_jobs/instruments.json b/tools/preview_jobs/instruments.json
index 92587d2..c448a0d 100644
--- a/tools/preview_jobs/instruments.json
+++ b/tools/preview_jobs/instruments.json
@@ -43,6 +43,19 @@
],
"samples": 64
},
+ {
+ "lib": "instruments/BALLOON/BALLOON.blend",
+ "coll": "INST_BALLOON",
+ "out": "instruments/preview/BALLOON.jpg",
+ "view": "front_x",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64,
+ "tight": 0.75,
+ "zfrac": 0.45
+ },
{
"lib": "instruments/CEIL/CEIL.blend",
"coll": "INST_CEIL",
@@ -89,8 +102,8 @@
},
{
"lib": "instruments/ECOR/ECOR.blend",
- "coll": "INST_ECOR_4m",
- "out": "instruments/preview/ECOR_4m.jpg",
+ "coll": "INST_ECOR",
+ "out": "instruments/preview/ECOR.jpg",
"view": "34",
"res": [
1024,
@@ -100,8 +113,8 @@
},
{
"lib": "instruments/ECOR/ECOR.blend",
- "coll": "INST_ECOR",
- "out": "instruments/preview/ECOR.jpg",
+ "coll": "INST_ECOR_4m",
+ "out": "instruments/preview/ECOR_4m.jpg",
"view": "34",
"res": [
1024,