diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..c362922
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+*.blend binary
+*.vdb binary
+*.exr binary
+*.png binary
+*.jpg binary
diff --git a/.gitignore b/.gitignore
index 68bc17f..a939a82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -158,3 +158,9 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
+
+# Blender
+*.blend1
+*.blend2
+*.blend@
+.DS_Store
diff --git a/README.md b/README.md
index d8c8445..f8d833a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,108 @@
-# ARM Repository Template
-Basic repository template with required security workflows
+# ablemodels — ARM Blender models
+
+Procedural Blender models of ARM Mobile Facility sites and instruments, built for the
+DERECHOS campaign (Iowa–Illinois). The repository is **self-contained and runnable**: every
+scene links its assets from the asset folders by relative path, so a fresh clone opens and
+renders with no missing files.
+
+Requires **Blender 5.2 LTS** or newer (files were written with 5.2.3).
+
+
+
+| | | |
+|---|---|---|
+|
|
|
|
+| CM1 supercell over M1 (`scenes/tests/m1_supercell.blend`) | S2, Washington Municipal Airport | AMF2 container row at M1 |
+
+## Layout
+
+| 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 |
+| [`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 |
+| [`plants/`](plants/) | Corn and soybean variants, lawn and tall grass, tree sets (standard and high-detail), native prairie flowers |
+| [`clouds/`](clouds/) | Mature-cloud snapshots as OpenVDB volumes with their shaders: LASSO SGP cumulus, UCLA cumulus, CM1 BOMEX cumulus, CM1 supercells |
+| [`scenes/`](scenes/) | The three DERECHOS sites (M1, S1, S2) and test scenes; site-specific terrain/vegetation layers live in `scenes//layers/` |
+| [`textures/`](textures/) | Shared image textures (PBR material sets, NAIP orthoimagery, sign and flag artwork) |
+| [`tools/`](tools/) | Scripts that built this library from the original working files, plus build/verification reports |
+
+Every asset folder has a `preview/` subfolder with renders of what it contains, and a
+`README.md` catalogue (file, collection name, preview).
+
+### Scenes
+
+| Scene | Site |
+|---|---|
+| `scenes/M1/M1.blend` | M1 — main AMF2 site on the farm (instrument field, AMF2 containers, farmstead, fields) |
+| `scenes/S1/S1.blend` | S1 — Muscatine Island Research Farm (ECORs in corn and soy, SEBS, center pivots, Louisa plant on the horizon) |
+| `scenes/S2/S2.blend` | S2 — Washington Municipal Airport, KAWG (X/Ka SACR, C-SAPR2, AWOS, Challenger 850) |
+| `scenes/tests/instrument_lineup.blend` | Every instrument asset in a row |
+| `scenes/tests/m1_supercell.blend` | M1 with the CM1 100 m supercell (snapshot `c100_t25`) placed as in the storm renders |
+| `scenes/tests/m1_lasso_cumulus.blend` | M1 under the LASSO SGP cumulus field |
+| `scenes/tests/s1_ucla_cumulus.blend` | S1 under the UCLA LES cumulus field |
+
+## How the linking works
+
+Blender can **link** data from another `.blend` instead of copying it
+(*File → Link*). A linked collection stays read-only in the scene and is re-read from its
+source file every time the scene opens, so fixing `instruments/KAZR/KAZR.blend` updates every
+scene that uses the KAZR. All paths are stored relative to the file (`//../../instruments/...`),
+which is what makes the repository portable.
+
+Re-usable assets (instruments, containers, vehicles, pivots, flagpole) are stored with their
+root at the origin. Scenes place them with a **collection-instance empty** (named
+`_inst`) carrying the location and rotation, so one asset can be placed many
+times. Site-specific layers (terrain, fields, farmstead buildings, vegetation scatter) are
+stored in their real-world site coordinates and linked in place.
+
+Common tasks:
+
+* **Use an asset in your own scene:** *File → Link*, browse into the `.blend`, open
+ `Collection`, pick the collection (e.g. `INST_KAZR`). It arrives as an instance you can move.
+* **Asset Browser:** add the repository root under *Preferences → File Paths → Asset
+ Libraries*; all asset collections are marked as assets.
+* **Pose a linked asset** (C-SAPR2 azimuth/elevation, Challenger 850 gear, flaps and fans,
+ AWOS wind vane): right-click the instance → *Library Override → Make → Selected & Content*,
+ then edit the custom properties on its `*_root` object; drivers on the parts read those
+ properties. The SACR azimuth/elevation is keyframed on `SACR_Azimuth`/`SACR_Elevation`.
+* **Edit an asset:** open its own `.blend` and save; every scene picks up the change.
+* **Make a copy you can edit locally:** *Object → Apply → Make Instances Real*, or
+ *Object → Relations → Make Local*.
+
+## Clouds
+
+Each cloud `.blend` has one collection per snapshot (`CLOUD_`); the VDB files sit next
+to it. Volume objects read the model's own grids (CM1: `cloud`, `rain`, `ice`, `snow`,
+`graup`, `cice`; LES: `lwc`). The CM1 storm shader renders each hydrometeor species with its
+own scattering and uses the raw model fields (no procedural noise).
+
+VDBs larger than 50 MB (the three 100 m supercell snapshots) were rewritten with 16-bit
+half-float storage to stay under GitHub's 100 MB file limit. Grid resolution, extent and all
+species are unchanged; half precision keeps about three significant digits of the mixing
+ratio, below what a volume render can show. Nothing was resampled.
+
+## Provenance and verification
+
+The library was generated from the original monolithic working files (`farm_v5.blend`,
+`mirf_s1.blend`, `airport_awg.blend` and the cloud scenes) by the scripts in `tools/`
+(`build_m1.py`, `build_s1.py`, `build_s2.py`, `build_clouds.py`, `build_tests.py`). The
+extraction is lossless: each rebuilt scene was evaluated against its source and has the same
+number of mesh instances, the same total vertex count and identical summed world positions
+(reports in `tools/reports/`), and test renders match the originals to within sampling noise.
+Text objects that used macOS system fonts were converted to meshes so they render the same
+on any OS.
+
+Deliberate differences from the working files:
+
+* At M1 the SACR container instance is hidden (the SACR moved to S2 for DERECHOS).
+* Instruments, containers and vehicles are instanced rather than embedded (see above).
+
+## Licences of third-party content
+
+* PBR texture sets in `textures/pbr/` are from [ambientCG](https://ambientcg.com) (CC0).
+* Orthoimagery in `textures/imagery/` is USDA NAIP (public domain); the M1 height fields
+ (`hf_*.exr`) are terrain elevation grids, stored with lossless ZIP compression.
+* Site signs and flags (`textures/signs/`, `textures/flags/`) carry partner and agency logos;
+ check their usage rules before reusing them outside DERECHOS/ARM material.
diff --git a/buildings/README.md b/buildings/README.md
new file mode 100644
index 0000000..62b196e
--- /dev/null
+++ b/buildings/README.md
@@ -0,0 +1,16 @@
+# Buildings and structures
+
+Site buildings are stored in site coordinates; the flagpole and center pivots are re-usable (root at the origin).
+
+| Preview | Collection | File |
+|---|---|---|
+|
| `Farmstead` | [`m1_farmstead.blend`](m1_farmstead.blend) (0.3 MB) |
+|
| `Flagpole` | [`flagpole.blend`](flagpole.blend) (0.2 MB) |
+|
| `SIGN_M1` | [`signs/sign_M1.blend`](signs/sign_M1.blend) (0.9 MB) |
+|
| `SIGN_S1` | [`signs/sign_S1.blend`](signs/sign_S1.blend) (0.9 MB) |
+|
| `SIGN_S2` | [`signs/sign_S2.blend`](signs/sign_S2.blend) (0.9 MB) |
+|
| `Pivot_P3` | [`center_pivots.blend`](center_pivots.blend) (1.5 MB) |
+|
| `Pivot_P1` | [`center_pivots.blend`](center_pivots.blend) (1.5 MB) |
+|
| `LOUISA_Station` | [`louisa_station.blend`](louisa_station.blend) (0.4 MB) |
+|
| `S1_MIRF_Buildings` | [`s1_mirf_buildings.blend`](s1_mirf_buildings.blend) (0.1 MB) |
+|
| `S2_KAWG_Buildings` | [`s2_kawg_buildings.blend`](s2_kawg_buildings.blend) (1.8 MB) |
diff --git a/buildings/center_pivots.blend b/buildings/center_pivots.blend
new file mode 100644
index 0000000..ebd12ef
Binary files /dev/null and b/buildings/center_pivots.blend differ
diff --git a/buildings/flagpole.blend b/buildings/flagpole.blend
new file mode 100644
index 0000000..7a8f0eb
Binary files /dev/null and b/buildings/flagpole.blend differ
diff --git a/buildings/louisa_station.blend b/buildings/louisa_station.blend
new file mode 100644
index 0000000..80e7dcb
Binary files /dev/null and b/buildings/louisa_station.blend differ
diff --git a/buildings/m1_farmstead.blend b/buildings/m1_farmstead.blend
new file mode 100644
index 0000000..4d36edc
Binary files /dev/null and b/buildings/m1_farmstead.blend differ
diff --git a/buildings/preview/Farmstead.jpg b/buildings/preview/Farmstead.jpg
new file mode 100644
index 0000000..0cc5610
Binary files /dev/null and b/buildings/preview/Farmstead.jpg differ
diff --git a/buildings/preview/Flagpole.jpg b/buildings/preview/Flagpole.jpg
new file mode 100644
index 0000000..e0e9afd
Binary files /dev/null and b/buildings/preview/Flagpole.jpg differ
diff --git a/buildings/preview/LOUISA_Station.jpg b/buildings/preview/LOUISA_Station.jpg
new file mode 100644
index 0000000..fac2938
Binary files /dev/null and b/buildings/preview/LOUISA_Station.jpg differ
diff --git a/buildings/preview/Pivot_P1.jpg b/buildings/preview/Pivot_P1.jpg
new file mode 100644
index 0000000..a626014
Binary files /dev/null and b/buildings/preview/Pivot_P1.jpg differ
diff --git a/buildings/preview/Pivot_P3.jpg b/buildings/preview/Pivot_P3.jpg
new file mode 100644
index 0000000..376ce93
Binary files /dev/null and b/buildings/preview/Pivot_P3.jpg differ
diff --git a/buildings/preview/S1_MIRF_Buildings.jpg b/buildings/preview/S1_MIRF_Buildings.jpg
new file mode 100644
index 0000000..2119248
Binary files /dev/null and b/buildings/preview/S1_MIRF_Buildings.jpg differ
diff --git a/buildings/preview/S2_KAWG_Buildings.jpg b/buildings/preview/S2_KAWG_Buildings.jpg
new file mode 100644
index 0000000..638d926
Binary files /dev/null and b/buildings/preview/S2_KAWG_Buildings.jpg differ
diff --git a/buildings/preview/SIGN_M1.jpg b/buildings/preview/SIGN_M1.jpg
new file mode 100644
index 0000000..2bf41b1
Binary files /dev/null and b/buildings/preview/SIGN_M1.jpg differ
diff --git a/buildings/preview/SIGN_S1.jpg b/buildings/preview/SIGN_S1.jpg
new file mode 100644
index 0000000..a82689c
Binary files /dev/null and b/buildings/preview/SIGN_S1.jpg differ
diff --git a/buildings/preview/SIGN_S2.jpg b/buildings/preview/SIGN_S2.jpg
new file mode 100644
index 0000000..3cc6fce
Binary files /dev/null and b/buildings/preview/SIGN_S2.jpg differ
diff --git a/buildings/s1_mirf_buildings.blend b/buildings/s1_mirf_buildings.blend
new file mode 100644
index 0000000..b9332fb
Binary files /dev/null and b/buildings/s1_mirf_buildings.blend differ
diff --git a/buildings/s2_kawg_buildings.blend b/buildings/s2_kawg_buildings.blend
new file mode 100644
index 0000000..b026eab
Binary files /dev/null and b/buildings/s2_kawg_buildings.blend differ
diff --git a/buildings/signs/sign_M1.blend b/buildings/signs/sign_M1.blend
new file mode 100644
index 0000000..b91d223
Binary files /dev/null and b/buildings/signs/sign_M1.blend differ
diff --git a/buildings/signs/sign_S1.blend b/buildings/signs/sign_S1.blend
new file mode 100644
index 0000000..dadc5b8
Binary files /dev/null and b/buildings/signs/sign_S1.blend differ
diff --git a/buildings/signs/sign_S2.blend b/buildings/signs/sign_S2.blend
new file mode 100644
index 0000000..29fd55b
Binary files /dev/null and b/buildings/signs/sign_S2.blend differ
diff --git a/clouds/README.md b/clouds/README.md
new file mode 100644
index 0000000..0e24dd4
--- /dev/null
+++ b/clouds/README.md
@@ -0,0 +1,37 @@
+# Clouds
+
+Mature snapshots; one collection per snapshot, VDB next to the .blend. Tiled LES fields (LASSO 3x3, UCLA 7x7) keep their periodic tiling.
+
+| Preview | Collection | File |
+|---|---|---|
+|
| `CLOUD_bomex_t360` | [`cm1_bomex_cumulus/cm1_bomex_cumulus.blend`](cm1_bomex_cumulus/cm1_bomex_cumulus.blend) (0.1 MB) |
+|
| `CLOUD_bomex_t300` | [`cm1_bomex_cumulus/cm1_bomex_cumulus.blend`](cm1_bomex_cumulus/cm1_bomex_cumulus.blend) (0.1 MB) |
+|
| `CLOUD_bomex_t240` | [`cm1_bomex_cumulus/cm1_bomex_cumulus.blend`](cm1_bomex_cumulus/cm1_bomex_cumulus.blend) (0.1 MB) |
+|
| `CLOUD_supercell_s250_t60` | [`cm1_supercell/cm1_supercell.blend`](cm1_supercell/cm1_supercell.blend) (0.1 MB) |
+|
| `CLOUD_supercell_s250_t45` | [`cm1_supercell/cm1_supercell.blend`](cm1_supercell/cm1_supercell.blend) (0.1 MB) |
+|
| `CLOUD_supercell_s100_t60` | [`cm1_supercell/cm1_supercell.blend`](cm1_supercell/cm1_supercell.blend) (0.1 MB) |
+|
| `CLOUD_supercell_s100_t45` | [`cm1_supercell/cm1_supercell.blend`](cm1_supercell/cm1_supercell.blend) (0.1 MB) |
+|
| `CLOUD_supercell_c100_t25` | [`cm1_supercell/cm1_supercell.blend`](cm1_supercell/cm1_supercell.blend) (0.1 MB) |
+|
| `CLOUD_lasso_2000` | [`lasso_sgp_20170403/lasso_sgp_20170403.blend`](lasso_sgp_20170403/lasso_sgp_20170403.blend) (0.1 MB) |
+|
| `CLOUD_lasso_1900` | [`lasso_sgp_20170403/lasso_sgp_20170403.blend`](lasso_sgp_20170403/lasso_sgp_20170403.blend) (0.1 MB) |
+|
| `CLOUD_lasso_1800` | [`lasso_sgp_20170403/lasso_sgp_20170403.blend`](lasso_sgp_20170403/lasso_sgp_20170403.blend) (0.1 MB) |
+|
| `CLOUD_lasso_1740` | [`lasso_sgp_20170403/lasso_sgp_20170403.blend`](lasso_sgp_20170403/lasso_sgp_20170403.blend) (0.1 MB) |
+|
| `CLOUD_lasso8_2040` | [`lasso_sgp_20170403/lasso_sgp_20170403.blend`](lasso_sgp_20170403/lasso_sgp_20170403.blend) (0.1 MB) |
+|
| `CLOUD_ucla_t6600` | [`ucla_cumulus/ucla_cumulus.blend`](ucla_cumulus/ucla_cumulus.blend) (0.1 MB) |
+
+VDB files:
+
+* `cm1_bomex_cumulus/bomex50_t240_x5.vdb` (2.9 MB)
+* `cm1_bomex_cumulus/bomex50_t300_x5.vdb` (3.2 MB)
+* `cm1_bomex_cumulus/bomex50_t360_x5.vdb` (2.4 MB)
+* `cm1_supercell/c100_t25_sp.vdb` (80.8 MB)
+* `cm1_supercell/s100_t45_sp.vdb` (42.4 MB)
+* `cm1_supercell/s100_t60_sp.vdb` (77.5 MB)
+* `cm1_supercell/s250_t45_sp.vdb` (5.5 MB)
+* `cm1_supercell/s250_t60_sp.vdb` (12.6 MB)
+* `lasso_sgp_20170403/lasso8_2040_lwc.vdb` (20.2 MB)
+* `lasso_sgp_20170403/lasso_1740_lwc.vdb` (2.2 MB)
+* `lasso_sgp_20170403/lwc_0145.vdb` (2.1 MB)
+* `lasso_sgp_20170403/lwc_0193.vdb` (1.2 MB)
+* `lasso_sgp_20170403/lwc_0241.vdb` (0.4 MB)
+* `ucla_cumulus/ucla_t6600_m.vdb` (0.5 MB)
diff --git a/clouds/cm1_bomex_cumulus/bomex50_t240_x5.vdb b/clouds/cm1_bomex_cumulus/bomex50_t240_x5.vdb
new file mode 100644
index 0000000..d4e0658
Binary files /dev/null and b/clouds/cm1_bomex_cumulus/bomex50_t240_x5.vdb differ
diff --git a/clouds/cm1_bomex_cumulus/bomex50_t300_x5.vdb b/clouds/cm1_bomex_cumulus/bomex50_t300_x5.vdb
new file mode 100644
index 0000000..c1fe48d
Binary files /dev/null and b/clouds/cm1_bomex_cumulus/bomex50_t300_x5.vdb differ
diff --git a/clouds/cm1_bomex_cumulus/bomex50_t360_x5.vdb b/clouds/cm1_bomex_cumulus/bomex50_t360_x5.vdb
new file mode 100644
index 0000000..ffb378d
Binary files /dev/null and b/clouds/cm1_bomex_cumulus/bomex50_t360_x5.vdb differ
diff --git a/clouds/cm1_bomex_cumulus/cm1_bomex_cumulus.blend b/clouds/cm1_bomex_cumulus/cm1_bomex_cumulus.blend
new file mode 100644
index 0000000..ac4e6ee
Binary files /dev/null and b/clouds/cm1_bomex_cumulus/cm1_bomex_cumulus.blend differ
diff --git a/clouds/cm1_supercell/c100_t25_sp.vdb b/clouds/cm1_supercell/c100_t25_sp.vdb
new file mode 100644
index 0000000..40f4c05
Binary files /dev/null and b/clouds/cm1_supercell/c100_t25_sp.vdb differ
diff --git a/clouds/cm1_supercell/cm1_supercell.blend b/clouds/cm1_supercell/cm1_supercell.blend
new file mode 100644
index 0000000..5b90b9d
Binary files /dev/null and b/clouds/cm1_supercell/cm1_supercell.blend differ
diff --git a/clouds/cm1_supercell/s100_t45_sp.vdb b/clouds/cm1_supercell/s100_t45_sp.vdb
new file mode 100644
index 0000000..87f28f7
Binary files /dev/null and b/clouds/cm1_supercell/s100_t45_sp.vdb differ
diff --git a/clouds/cm1_supercell/s100_t60_sp.vdb b/clouds/cm1_supercell/s100_t60_sp.vdb
new file mode 100644
index 0000000..d877923
Binary files /dev/null and b/clouds/cm1_supercell/s100_t60_sp.vdb differ
diff --git a/clouds/cm1_supercell/s250_t45_sp.vdb b/clouds/cm1_supercell/s250_t45_sp.vdb
new file mode 100644
index 0000000..e0053e3
Binary files /dev/null and b/clouds/cm1_supercell/s250_t45_sp.vdb differ
diff --git a/clouds/cm1_supercell/s250_t60_sp.vdb b/clouds/cm1_supercell/s250_t60_sp.vdb
new file mode 100644
index 0000000..a480305
Binary files /dev/null and b/clouds/cm1_supercell/s250_t60_sp.vdb differ
diff --git a/clouds/lasso_sgp_20170403/lasso8_2040_lwc.vdb b/clouds/lasso_sgp_20170403/lasso8_2040_lwc.vdb
new file mode 100644
index 0000000..083ae20
Binary files /dev/null and b/clouds/lasso_sgp_20170403/lasso8_2040_lwc.vdb differ
diff --git a/clouds/lasso_sgp_20170403/lasso_1740_lwc.vdb b/clouds/lasso_sgp_20170403/lasso_1740_lwc.vdb
new file mode 100644
index 0000000..b433b57
Binary files /dev/null and b/clouds/lasso_sgp_20170403/lasso_1740_lwc.vdb differ
diff --git a/clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend b/clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend
new file mode 100644
index 0000000..260ade8
Binary files /dev/null and b/clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend differ
diff --git a/clouds/lasso_sgp_20170403/lwc_0145.vdb b/clouds/lasso_sgp_20170403/lwc_0145.vdb
new file mode 100644
index 0000000..af3c12d
Binary files /dev/null and b/clouds/lasso_sgp_20170403/lwc_0145.vdb differ
diff --git a/clouds/lasso_sgp_20170403/lwc_0193.vdb b/clouds/lasso_sgp_20170403/lwc_0193.vdb
new file mode 100644
index 0000000..c0339c0
Binary files /dev/null and b/clouds/lasso_sgp_20170403/lwc_0193.vdb differ
diff --git a/clouds/lasso_sgp_20170403/lwc_0241.vdb b/clouds/lasso_sgp_20170403/lwc_0241.vdb
new file mode 100644
index 0000000..7cd6385
Binary files /dev/null and b/clouds/lasso_sgp_20170403/lwc_0241.vdb differ
diff --git a/clouds/preview/bomex_t240.jpg b/clouds/preview/bomex_t240.jpg
new file mode 100644
index 0000000..4172a89
Binary files /dev/null and b/clouds/preview/bomex_t240.jpg differ
diff --git a/clouds/preview/bomex_t300.jpg b/clouds/preview/bomex_t300.jpg
new file mode 100644
index 0000000..7e78fda
Binary files /dev/null and b/clouds/preview/bomex_t300.jpg differ
diff --git a/clouds/preview/bomex_t360.jpg b/clouds/preview/bomex_t360.jpg
new file mode 100644
index 0000000..1d3c37b
Binary files /dev/null and b/clouds/preview/bomex_t360.jpg differ
diff --git a/clouds/preview/lasso8_2040.jpg b/clouds/preview/lasso8_2040.jpg
new file mode 100644
index 0000000..c1135da
Binary files /dev/null and b/clouds/preview/lasso8_2040.jpg differ
diff --git a/clouds/preview/lasso_1740.jpg b/clouds/preview/lasso_1740.jpg
new file mode 100644
index 0000000..d137575
Binary files /dev/null and b/clouds/preview/lasso_1740.jpg differ
diff --git a/clouds/preview/lasso_1800.jpg b/clouds/preview/lasso_1800.jpg
new file mode 100644
index 0000000..e9940c3
Binary files /dev/null and b/clouds/preview/lasso_1800.jpg differ
diff --git a/clouds/preview/lasso_1900.jpg b/clouds/preview/lasso_1900.jpg
new file mode 100644
index 0000000..67e3238
Binary files /dev/null and b/clouds/preview/lasso_1900.jpg differ
diff --git a/clouds/preview/lasso_2000.jpg b/clouds/preview/lasso_2000.jpg
new file mode 100644
index 0000000..9b119d4
Binary files /dev/null and b/clouds/preview/lasso_2000.jpg differ
diff --git a/clouds/preview/supercell_c100_t25.jpg b/clouds/preview/supercell_c100_t25.jpg
new file mode 100644
index 0000000..360c685
Binary files /dev/null and b/clouds/preview/supercell_c100_t25.jpg differ
diff --git a/clouds/preview/supercell_s100_t45.jpg b/clouds/preview/supercell_s100_t45.jpg
new file mode 100644
index 0000000..8892002
Binary files /dev/null and b/clouds/preview/supercell_s100_t45.jpg differ
diff --git a/clouds/preview/supercell_s100_t60.jpg b/clouds/preview/supercell_s100_t60.jpg
new file mode 100644
index 0000000..45f4792
Binary files /dev/null and b/clouds/preview/supercell_s100_t60.jpg differ
diff --git a/clouds/preview/supercell_s250_t45.jpg b/clouds/preview/supercell_s250_t45.jpg
new file mode 100644
index 0000000..a4edd2f
Binary files /dev/null and b/clouds/preview/supercell_s250_t45.jpg differ
diff --git a/clouds/preview/supercell_s250_t60.jpg b/clouds/preview/supercell_s250_t60.jpg
new file mode 100644
index 0000000..6c01225
Binary files /dev/null and b/clouds/preview/supercell_s250_t60.jpg differ
diff --git a/clouds/preview/ucla_t6600.jpg b/clouds/preview/ucla_t6600.jpg
new file mode 100644
index 0000000..74ac03a
Binary files /dev/null and b/clouds/preview/ucla_t6600.jpg differ
diff --git a/clouds/ucla_cumulus/ucla_cumulus.blend b/clouds/ucla_cumulus/ucla_cumulus.blend
new file mode 100644
index 0000000..e7377ba
Binary files /dev/null and b/clouds/ucla_cumulus/ucla_cumulus.blend differ
diff --git a/clouds/ucla_cumulus/ucla_t6600_m.vdb b/clouds/ucla_cumulus/ucla_t6600_m.vdb
new file mode 100644
index 0000000..8fe55ba
Binary files /dev/null and b/clouds/ucla_cumulus/ucla_t6600_m.vdb differ
diff --git a/containers/README.md b/containers/README.md
new file mode 100644
index 0000000..967ea45
--- /dev/null
+++ b/containers/README.md
@@ -0,0 +1,18 @@
+# Containers
+
+AMF2 containers and shelters with their roof-mounted hardware. `CONT_7_SACR_S2` is the SACR container as configured at S2 (KAWG).
+
+| Preview | Collection | File |
+|---|---|---|
+|
| `CONT_RentalContainer` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_OfficeShelter` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_AOS_Platform` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_8_STORAGE` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_7_SACR` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_6_KAZR` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_5_LIDAR` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_4_RADIOMETERS` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_3_OPS` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_2_AOS_B` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_1_AOS_A` | [`amf2_containers.blend`](amf2_containers.blend) (0.2 MB) |
+|
| `CONT_7_SACR_S2` | [`sacr_container_s2.blend`](sacr_container_s2.blend) (0.1 MB) |
diff --git a/containers/amf2_containers.blend b/containers/amf2_containers.blend
new file mode 100644
index 0000000..bd2015b
Binary files /dev/null and b/containers/amf2_containers.blend differ
diff --git a/containers/preview/CONT_1_AOS_A.jpg b/containers/preview/CONT_1_AOS_A.jpg
new file mode 100644
index 0000000..4d70211
Binary files /dev/null and b/containers/preview/CONT_1_AOS_A.jpg differ
diff --git a/containers/preview/CONT_2_AOS_B.jpg b/containers/preview/CONT_2_AOS_B.jpg
new file mode 100644
index 0000000..55446c8
Binary files /dev/null and b/containers/preview/CONT_2_AOS_B.jpg differ
diff --git a/containers/preview/CONT_3_OPS.jpg b/containers/preview/CONT_3_OPS.jpg
new file mode 100644
index 0000000..f8e4ea5
Binary files /dev/null and b/containers/preview/CONT_3_OPS.jpg differ
diff --git a/containers/preview/CONT_4_RADIOMETERS.jpg b/containers/preview/CONT_4_RADIOMETERS.jpg
new file mode 100644
index 0000000..92177e5
Binary files /dev/null and b/containers/preview/CONT_4_RADIOMETERS.jpg differ
diff --git a/containers/preview/CONT_5_LIDAR.jpg b/containers/preview/CONT_5_LIDAR.jpg
new file mode 100644
index 0000000..555a511
Binary files /dev/null and b/containers/preview/CONT_5_LIDAR.jpg differ
diff --git a/containers/preview/CONT_6_KAZR.jpg b/containers/preview/CONT_6_KAZR.jpg
new file mode 100644
index 0000000..6d6411b
Binary files /dev/null and b/containers/preview/CONT_6_KAZR.jpg differ
diff --git a/containers/preview/CONT_7_SACR.jpg b/containers/preview/CONT_7_SACR.jpg
new file mode 100644
index 0000000..4e29353
Binary files /dev/null and b/containers/preview/CONT_7_SACR.jpg differ
diff --git a/containers/preview/CONT_7_SACR_S2.jpg b/containers/preview/CONT_7_SACR_S2.jpg
new file mode 100644
index 0000000..742af49
Binary files /dev/null and b/containers/preview/CONT_7_SACR_S2.jpg differ
diff --git a/containers/preview/CONT_8_STORAGE.jpg b/containers/preview/CONT_8_STORAGE.jpg
new file mode 100644
index 0000000..b982aae
Binary files /dev/null and b/containers/preview/CONT_8_STORAGE.jpg differ
diff --git a/containers/preview/CONT_AOS_Platform.jpg b/containers/preview/CONT_AOS_Platform.jpg
new file mode 100644
index 0000000..d58fdb6
Binary files /dev/null and b/containers/preview/CONT_AOS_Platform.jpg differ
diff --git a/containers/preview/CONT_OfficeShelter.jpg b/containers/preview/CONT_OfficeShelter.jpg
new file mode 100644
index 0000000..d39e953
Binary files /dev/null and b/containers/preview/CONT_OfficeShelter.jpg differ
diff --git a/containers/preview/CONT_RentalContainer.jpg b/containers/preview/CONT_RentalContainer.jpg
new file mode 100644
index 0000000..5a1c229
Binary files /dev/null and b/containers/preview/CONT_RentalContainer.jpg differ
diff --git a/containers/sacr_container_s2.blend b/containers/sacr_container_s2.blend
new file mode 100644
index 0000000..cae6a01
Binary files /dev/null and b/containers/sacr_container_s2.blend differ
diff --git a/instruments/AERI/AERI.blend b/instruments/AERI/AERI.blend
new file mode 100644
index 0000000..1b9320f
Binary files /dev/null and b/instruments/AERI/AERI.blend differ
diff --git a/instruments/AOSMET/AOSMET.blend b/instruments/AOSMET/AOSMET.blend
new file mode 100644
index 0000000..b94bfff
Binary files /dev/null and b/instruments/AOSMET/AOSMET.blend differ
diff --git a/instruments/ASI/ASI.blend b/instruments/ASI/ASI.blend
new file mode 100644
index 0000000..036351e
Binary files /dev/null and b/instruments/ASI/ASI.blend differ
diff --git a/instruments/AWOS/AWOS.blend b/instruments/AWOS/AWOS.blend
new file mode 100644
index 0000000..c13a8e1
Binary files /dev/null and b/instruments/AWOS/AWOS.blend differ
diff --git a/instruments/CEIL/CEIL.blend b/instruments/CEIL/CEIL.blend
new file mode 100644
index 0000000..dacf413
Binary files /dev/null and b/instruments/CEIL/CEIL.blend differ
diff --git a/instruments/CSAPR2/CSAPR2.blend b/instruments/CSAPR2/CSAPR2.blend
new file mode 100644
index 0000000..6f39c17
Binary files /dev/null and b/instruments/CSAPR2/CSAPR2.blend differ
diff --git a/instruments/CSPHOT/CSPHOT.blend b/instruments/CSPHOT/CSPHOT.blend
new file mode 100644
index 0000000..8e34c2b
Binary files /dev/null and b/instruments/CSPHOT/CSPHOT.blend differ
diff --git a/instruments/DL/DL.blend b/instruments/DL/DL.blend
new file mode 100644
index 0000000..4c0b67f
Binary files /dev/null and b/instruments/DL/DL.blend differ
diff --git a/instruments/ECOR/ECOR.blend b/instruments/ECOR/ECOR.blend
new file mode 100644
index 0000000..f59aa69
Binary files /dev/null and b/instruments/ECOR/ECOR.blend differ
diff --git a/instruments/IRT/IRT.blend b/instruments/IRT/IRT.blend
new file mode 100644
index 0000000..06d9923
Binary files /dev/null and b/instruments/IRT/IRT.blend differ
diff --git a/instruments/KAZR/KAZR.blend b/instruments/KAZR/KAZR.blend
new file mode 100644
index 0000000..3b4d9fb
Binary files /dev/null and b/instruments/KAZR/KAZR.blend differ
diff --git a/instruments/LDIS/LDIS.blend b/instruments/LDIS/LDIS.blend
new file mode 100644
index 0000000..18657e7
Binary files /dev/null and b/instruments/LDIS/LDIS.blend differ
diff --git a/instruments/MAWS/MAWS.blend b/instruments/MAWS/MAWS.blend
new file mode 100644
index 0000000..da035bf
Binary files /dev/null and b/instruments/MAWS/MAWS.blend differ
diff --git a/instruments/MET/MET.blend b/instruments/MET/MET.blend
new file mode 100644
index 0000000..f5fd964
Binary files /dev/null and b/instruments/MET/MET.blend differ
diff --git a/instruments/MFR/MFR.blend b/instruments/MFR/MFR.blend
new file mode 100644
index 0000000..e276bad
Binary files /dev/null and b/instruments/MFR/MFR.blend differ
diff --git a/instruments/MFRSR/MFRSR.blend b/instruments/MFRSR/MFRSR.blend
new file mode 100644
index 0000000..33b1e32
Binary files /dev/null and b/instruments/MFRSR/MFRSR.blend differ
diff --git a/instruments/MPL/MPL.blend b/instruments/MPL/MPL.blend
new file mode 100644
index 0000000..f7e2279
Binary files /dev/null and b/instruments/MPL/MPL.blend differ
diff --git a/instruments/MWR3C/MWR3C.blend b/instruments/MWR3C/MWR3C.blend
new file mode 100644
index 0000000..a4bd282
Binary files /dev/null and b/instruments/MWR3C/MWR3C.blend differ
diff --git a/instruments/README.md b/instruments/README.md
new file mode 100644
index 0000000..c6a9d25
--- /dev/null
+++ b/instruments/README.md
@@ -0,0 +1,31 @@
+# Instruments
+
+One file per instrument; link the `INST_*` (or `CSAPR2`) collection. Root at the origin.
+
+| 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_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_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) |
+|
| `INST_MAWS` | [`MAWS/MAWS.blend`](MAWS/MAWS.blend) (0.2 MB) |
+|
| `INST_MET` | [`MET/MET.blend`](MET/MET.blend) (0.3 MB) |
+|
| `INST_MFR` | [`MFR/MFR.blend`](MFR/MFR.blend) (0.1 MB) |
+|
| `INST_MFRSR` | [`MFRSR/MFRSR.blend`](MFRSR/MFRSR.blend) (0.1 MB) |
+|
| `INST_MPL` | [`MPL/MPL.blend`](MPL/MPL.blend) (0.1 MB) |
+|
| `INST_MWR3C` | [`MWR3C/MWR3C.blend`](MWR3C/MWR3C.blend) (0.2 MB) |
+|
| `INST_RWP` | [`RWP/RWP.blend`](RWP/RWP.blend) (0.3 MB) |
+|
| `INST_SEBS` | [`SEBS/SEBS.blend`](SEBS/SEBS.blend) (0.2 MB) |
+|
| `INST_SIRS` | [`SIRS/SIRS.blend`](SIRS/SIRS.blend) (0.3 MB) |
+|
| `INST_SONDE` | [`SONDE/SONDE.blend`](SONDE/SONDE.blend) (0.2 MB) |
+|
| `INST_VDIS` | [`VDIS/VDIS.blend`](VDIS/VDIS.blend) (0.1 MB) |
+|
| `INST_WB` | [`WB/WB.blend`](WB/WB.blend) (0.2 MB) |
diff --git a/instruments/RWP/RWP.blend b/instruments/RWP/RWP.blend
new file mode 100644
index 0000000..eea1375
Binary files /dev/null and b/instruments/RWP/RWP.blend differ
diff --git a/instruments/SEBS/SEBS.blend b/instruments/SEBS/SEBS.blend
new file mode 100644
index 0000000..f583b04
Binary files /dev/null and b/instruments/SEBS/SEBS.blend differ
diff --git a/instruments/SIRS/SIRS.blend b/instruments/SIRS/SIRS.blend
new file mode 100644
index 0000000..87a42c0
Binary files /dev/null and b/instruments/SIRS/SIRS.blend differ
diff --git a/instruments/SONDE/SONDE.blend b/instruments/SONDE/SONDE.blend
new file mode 100644
index 0000000..2798460
Binary files /dev/null and b/instruments/SONDE/SONDE.blend differ
diff --git a/instruments/VDIS/VDIS.blend b/instruments/VDIS/VDIS.blend
new file mode 100644
index 0000000..4504c02
Binary files /dev/null and b/instruments/VDIS/VDIS.blend differ
diff --git a/instruments/WB/WB.blend b/instruments/WB/WB.blend
new file mode 100644
index 0000000..41c34c6
Binary files /dev/null and b/instruments/WB/WB.blend differ
diff --git a/instruments/preview/AERI.jpg b/instruments/preview/AERI.jpg
new file mode 100644
index 0000000..f8088cf
Binary files /dev/null and b/instruments/preview/AERI.jpg differ
diff --git a/instruments/preview/AOSMET.jpg b/instruments/preview/AOSMET.jpg
new file mode 100644
index 0000000..53a12fb
Binary files /dev/null and b/instruments/preview/AOSMET.jpg differ
diff --git a/instruments/preview/ASI.jpg b/instruments/preview/ASI.jpg
new file mode 100644
index 0000000..a2b6dfd
Binary files /dev/null and b/instruments/preview/ASI.jpg differ
diff --git a/instruments/preview/AWOS.jpg b/instruments/preview/AWOS.jpg
new file mode 100644
index 0000000..2053dd7
Binary files /dev/null and b/instruments/preview/AWOS.jpg differ
diff --git a/instruments/preview/CEIL.jpg b/instruments/preview/CEIL.jpg
new file mode 100644
index 0000000..ee59edd
Binary files /dev/null and b/instruments/preview/CEIL.jpg differ
diff --git a/instruments/preview/CSAPR2.jpg b/instruments/preview/CSAPR2.jpg
new file mode 100644
index 0000000..e336c98
Binary files /dev/null and b/instruments/preview/CSAPR2.jpg differ
diff --git a/instruments/preview/CSPHOT.jpg b/instruments/preview/CSPHOT.jpg
new file mode 100644
index 0000000..ef06bf3
Binary files /dev/null and b/instruments/preview/CSPHOT.jpg differ
diff --git a/instruments/preview/DL.jpg b/instruments/preview/DL.jpg
new file mode 100644
index 0000000..e7610ff
Binary files /dev/null and b/instruments/preview/DL.jpg differ
diff --git a/instruments/preview/ECOR.jpg b/instruments/preview/ECOR.jpg
new file mode 100644
index 0000000..9795630
Binary files /dev/null and b/instruments/preview/ECOR.jpg differ
diff --git a/instruments/preview/ECOR_4m.jpg b/instruments/preview/ECOR_4m.jpg
new file mode 100644
index 0000000..abb9d7b
Binary files /dev/null and b/instruments/preview/ECOR_4m.jpg differ
diff --git a/instruments/preview/IRT.jpg b/instruments/preview/IRT.jpg
new file mode 100644
index 0000000..d3a5b35
Binary files /dev/null and b/instruments/preview/IRT.jpg differ
diff --git a/instruments/preview/KAZR.jpg b/instruments/preview/KAZR.jpg
new file mode 100644
index 0000000..1378ecc
Binary files /dev/null and b/instruments/preview/KAZR.jpg differ
diff --git a/instruments/preview/LDIS.jpg b/instruments/preview/LDIS.jpg
new file mode 100644
index 0000000..fec9f61
Binary files /dev/null and b/instruments/preview/LDIS.jpg differ
diff --git a/instruments/preview/MAWS.jpg b/instruments/preview/MAWS.jpg
new file mode 100644
index 0000000..869297d
Binary files /dev/null and b/instruments/preview/MAWS.jpg differ
diff --git a/instruments/preview/MET.jpg b/instruments/preview/MET.jpg
new file mode 100644
index 0000000..e12f44a
Binary files /dev/null and b/instruments/preview/MET.jpg differ
diff --git a/instruments/preview/MFR.jpg b/instruments/preview/MFR.jpg
new file mode 100644
index 0000000..e4972be
Binary files /dev/null and b/instruments/preview/MFR.jpg differ
diff --git a/instruments/preview/MFRSR.jpg b/instruments/preview/MFRSR.jpg
new file mode 100644
index 0000000..3c12ec7
Binary files /dev/null and b/instruments/preview/MFRSR.jpg differ
diff --git a/instruments/preview/MPL.jpg b/instruments/preview/MPL.jpg
new file mode 100644
index 0000000..13207b3
Binary files /dev/null and b/instruments/preview/MPL.jpg differ
diff --git a/instruments/preview/MWR3C.jpg b/instruments/preview/MWR3C.jpg
new file mode 100644
index 0000000..609c5f3
Binary files /dev/null and b/instruments/preview/MWR3C.jpg differ
diff --git a/instruments/preview/RWP.jpg b/instruments/preview/RWP.jpg
new file mode 100644
index 0000000..91140b0
Binary files /dev/null and b/instruments/preview/RWP.jpg differ
diff --git a/instruments/preview/SEBS.jpg b/instruments/preview/SEBS.jpg
new file mode 100644
index 0000000..e129ee9
Binary files /dev/null and b/instruments/preview/SEBS.jpg differ
diff --git a/instruments/preview/SIRS.jpg b/instruments/preview/SIRS.jpg
new file mode 100644
index 0000000..e864c75
Binary files /dev/null and b/instruments/preview/SIRS.jpg differ
diff --git a/instruments/preview/SONDE.jpg b/instruments/preview/SONDE.jpg
new file mode 100644
index 0000000..7b63297
Binary files /dev/null and b/instruments/preview/SONDE.jpg differ
diff --git a/instruments/preview/VDIS.jpg b/instruments/preview/VDIS.jpg
new file mode 100644
index 0000000..58e2f14
Binary files /dev/null and b/instruments/preview/VDIS.jpg differ
diff --git a/instruments/preview/WB.jpg b/instruments/preview/WB.jpg
new file mode 100644
index 0000000..9f45e12
Binary files /dev/null and b/instruments/preview/WB.jpg differ
diff --git a/plants/README.md b/plants/README.md
new file mode 100644
index 0000000..6770a1d
--- /dev/null
+++ b/plants/README.md
@@ -0,0 +1,18 @@
+# Plants
+
+Prototype collections used by the geometry-node scatterers in the site layers. `_HD` sets are the high-detail trees used at S1/S2; `_S2` grasses are the KAWG lawn variants.
+
+| Preview | Collection | File |
+|---|---|---|
+|
| `CornVariants` | [`crops.blend`](crops.blend) (0.7 MB) |
+|
| `SoyVariants` | [`crops.blend`](crops.blend) (0.7 MB) |
+|
| `GrassVariants` | [`grasses.blend`](grasses.blend) (0.3 MB) |
+|
| `TallGrassVariants` | [`grasses.blend`](grasses.blend) (0.3 MB) |
+|
| `GrassVariants_S2` | [`grasses_s2.blend`](grasses_s2.blend) (0.3 MB) |
+|
| `TallGrassVariants_S2` | [`grasses_s2.blend`](grasses_s2.blend) (0.3 MB) |
+|
| `TreeVariants` | [`trees.blend`](trees.blend) (3.4 MB) |
+|
| `TV_Decid_HD` | [`trees_hd.blend`](trees_hd.blend) (42.9 MB) |
+|
| `TV_Spruce_HD` | [`trees_hd.blend`](trees_hd.blend) (42.9 MB) |
+|
| `TV_DecidBaked_HD` | [`trees_hd.blend`](trees_hd.blend) (42.9 MB) |
+|
| `TV_Far_HD` | [`trees_hd.blend`](trees_hd.blend) (42.9 MB) |
+|
| `NativeFlowerVariants` | [`native_flowers.blend`](native_flowers.blend) (0.2 MB) |
diff --git a/plants/crops.blend b/plants/crops.blend
new file mode 100644
index 0000000..b21c01f
Binary files /dev/null and b/plants/crops.blend differ
diff --git a/plants/grasses.blend b/plants/grasses.blend
new file mode 100644
index 0000000..24f3fb6
Binary files /dev/null and b/plants/grasses.blend differ
diff --git a/plants/grasses_s2.blend b/plants/grasses_s2.blend
new file mode 100644
index 0000000..6b7cebd
Binary files /dev/null and b/plants/grasses_s2.blend differ
diff --git a/plants/native_flowers.blend b/plants/native_flowers.blend
new file mode 100644
index 0000000..f9b6fb7
Binary files /dev/null and b/plants/native_flowers.blend differ
diff --git a/plants/preview/CornVariants.jpg b/plants/preview/CornVariants.jpg
new file mode 100644
index 0000000..5c92574
Binary files /dev/null and b/plants/preview/CornVariants.jpg differ
diff --git a/plants/preview/GrassVariants.jpg b/plants/preview/GrassVariants.jpg
new file mode 100644
index 0000000..b23bf11
Binary files /dev/null and b/plants/preview/GrassVariants.jpg differ
diff --git a/plants/preview/GrassVariants_S2.jpg b/plants/preview/GrassVariants_S2.jpg
new file mode 100644
index 0000000..1fcc2ab
Binary files /dev/null and b/plants/preview/GrassVariants_S2.jpg differ
diff --git a/plants/preview/NativeFlowerVariants.jpg b/plants/preview/NativeFlowerVariants.jpg
new file mode 100644
index 0000000..7614af9
Binary files /dev/null and b/plants/preview/NativeFlowerVariants.jpg differ
diff --git a/plants/preview/SoyVariants.jpg b/plants/preview/SoyVariants.jpg
new file mode 100644
index 0000000..d1fdb1d
Binary files /dev/null and b/plants/preview/SoyVariants.jpg differ
diff --git a/plants/preview/TV_DecidBaked_HD.jpg b/plants/preview/TV_DecidBaked_HD.jpg
new file mode 100644
index 0000000..98dee67
Binary files /dev/null and b/plants/preview/TV_DecidBaked_HD.jpg differ
diff --git a/plants/preview/TV_Decid_HD.jpg b/plants/preview/TV_Decid_HD.jpg
new file mode 100644
index 0000000..9b12d60
Binary files /dev/null and b/plants/preview/TV_Decid_HD.jpg differ
diff --git a/plants/preview/TV_Far_HD.jpg b/plants/preview/TV_Far_HD.jpg
new file mode 100644
index 0000000..f0492be
Binary files /dev/null and b/plants/preview/TV_Far_HD.jpg differ
diff --git a/plants/preview/TV_Spruce_HD.jpg b/plants/preview/TV_Spruce_HD.jpg
new file mode 100644
index 0000000..e0cc899
Binary files /dev/null and b/plants/preview/TV_Spruce_HD.jpg differ
diff --git a/plants/preview/TallGrassVariants.jpg b/plants/preview/TallGrassVariants.jpg
new file mode 100644
index 0000000..da374bf
Binary files /dev/null and b/plants/preview/TallGrassVariants.jpg differ
diff --git a/plants/preview/TallGrassVariants_S2.jpg b/plants/preview/TallGrassVariants_S2.jpg
new file mode 100644
index 0000000..336df80
Binary files /dev/null and b/plants/preview/TallGrassVariants_S2.jpg differ
diff --git a/plants/preview/TreeVariants.jpg b/plants/preview/TreeVariants.jpg
new file mode 100644
index 0000000..e1b86c8
Binary files /dev/null and b/plants/preview/TreeVariants.jpg differ
diff --git a/plants/trees.blend b/plants/trees.blend
new file mode 100644
index 0000000..fb73a19
Binary files /dev/null and b/plants/trees.blend differ
diff --git a/plants/trees_hd.blend b/plants/trees_hd.blend
new file mode 100644
index 0000000..c209c2d
Binary files /dev/null and b/plants/trees_hd.blend differ
diff --git a/scenes/M1/M1.blend b/scenes/M1/M1.blend
new file mode 100644
index 0000000..e34d96c
Binary files /dev/null and b/scenes/M1/M1.blend differ
diff --git a/scenes/M1/layers/m1_amf2_site.blend b/scenes/M1/layers/m1_amf2_site.blend
new file mode 100644
index 0000000..f7e05c2
Binary files /dev/null and b/scenes/M1/layers/m1_amf2_site.blend differ
diff --git a/scenes/M1/layers/m1_fields.blend b/scenes/M1/layers/m1_fields.blend
new file mode 100644
index 0000000..7046b57
Binary files /dev/null and b/scenes/M1/layers/m1_fields.blend differ
diff --git a/scenes/M1/layers/m1_ground.blend b/scenes/M1/layers/m1_ground.blend
new file mode 100644
index 0000000..053ae9e
Binary files /dev/null and b/scenes/M1/layers/m1_ground.blend differ
diff --git a/scenes/README.md b/scenes/README.md
new file mode 100644
index 0000000..9112867
--- /dev/null
+++ b/scenes/README.md
@@ -0,0 +1,30 @@
+# Scenes
+
+Each site scene is a thin file holding cameras, lights, world and render settings; everything else is linked from the asset folders and from `layers/` (terrain, fields, vegetation, site infrastructure in site coordinates).
+
+## 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)
+
+
+
+## S1
+
+`S1/S1.blend` (0.1 MB) — layers: `S1/layers/s1_terrain.blend` (41.2 MB), `S1/layers/s1_vegetation.blend` (8.4 MB)
+
+
+
+## S2
+
+`S2/S2.blend` (0.1 MB) — layers: `S2/layers/s2_power_roads.blend` (0.2 MB), `S2/layers/s2_terrain.blend` (47.9 MB), `S2/layers/s2_vegetation.blend` (40.9 MB)
+
+
+
+## Test scenes
+
+| Scene | Preview |
+|---|---|
+| `tests/instrument_lineup.blend` |
|
+| `tests/m1_lasso_cumulus.blend` |
|
+| `tests/m1_supercell.blend` |
|
+| `tests/s1_ucla_cumulus.blend` |
|
diff --git a/scenes/S1/S1.blend b/scenes/S1/S1.blend
new file mode 100644
index 0000000..1716efb
Binary files /dev/null and b/scenes/S1/S1.blend differ
diff --git a/scenes/S1/layers/s1_terrain.blend b/scenes/S1/layers/s1_terrain.blend
new file mode 100644
index 0000000..7dc530f
Binary files /dev/null and b/scenes/S1/layers/s1_terrain.blend differ
diff --git a/scenes/S1/layers/s1_vegetation.blend b/scenes/S1/layers/s1_vegetation.blend
new file mode 100644
index 0000000..59fbb3f
Binary files /dev/null and b/scenes/S1/layers/s1_vegetation.blend differ
diff --git a/scenes/S2/S2.blend b/scenes/S2/S2.blend
new file mode 100644
index 0000000..98d86c5
Binary files /dev/null and b/scenes/S2/S2.blend differ
diff --git a/scenes/S2/layers/s2_power_roads.blend b/scenes/S2/layers/s2_power_roads.blend
new file mode 100644
index 0000000..7894a81
Binary files /dev/null and b/scenes/S2/layers/s2_power_roads.blend differ
diff --git a/scenes/S2/layers/s2_terrain.blend b/scenes/S2/layers/s2_terrain.blend
new file mode 100644
index 0000000..898c782
Binary files /dev/null and b/scenes/S2/layers/s2_terrain.blend differ
diff --git a/scenes/S2/layers/s2_vegetation.blend b/scenes/S2/layers/s2_vegetation.blend
new file mode 100644
index 0000000..10ba276
Binary files /dev/null and b/scenes/S2/layers/s2_vegetation.blend differ
diff --git a/scenes/preview/M1_Cam_M1_Aerial_SE.jpg b/scenes/preview/M1_Cam_M1_Aerial_SE.jpg
new file mode 100644
index 0000000..8a31b41
Binary files /dev/null and b/scenes/preview/M1_Cam_M1_Aerial_SE.jpg differ
diff --git a/scenes/preview/M1_Cam_M1_Yard.jpg b/scenes/preview/M1_Cam_M1_Yard.jpg
new file mode 100644
index 0000000..0cc5610
Binary files /dev/null and b/scenes/preview/M1_Cam_M1_Yard.jpg differ
diff --git a/scenes/preview/M1_Cam_Sign_M1.jpg b/scenes/preview/M1_Cam_Sign_M1.jpg
new file mode 100644
index 0000000..7573a2b
Binary files /dev/null and b/scenes/preview/M1_Cam_Sign_M1.jpg differ
diff --git a/scenes/preview/M1_Camera_AMF2.jpg b/scenes/preview/M1_Camera_AMF2.jpg
new file mode 100644
index 0000000..86f168d
Binary files /dev/null and b/scenes/preview/M1_Camera_AMF2.jpg differ
diff --git a/scenes/preview/M1_Camera_MetTower.jpg b/scenes/preview/M1_Camera_MetTower.jpg
new file mode 100644
index 0000000..7f0804b
Binary files /dev/null and b/scenes/preview/M1_Camera_MetTower.jpg differ
diff --git a/scenes/preview/S1_Cam_Aerial_SW.jpg b/scenes/preview/S1_Cam_Aerial_SW.jpg
new file mode 100644
index 0000000..2119248
Binary files /dev/null and b/scenes/preview/S1_Cam_Aerial_SW.jpg differ
diff --git a/scenes/preview/S1_Cam_ECOR_Corn.jpg b/scenes/preview/S1_Cam_ECOR_Corn.jpg
new file mode 100644
index 0000000..795e11b
Binary files /dev/null and b/scenes/preview/S1_Cam_ECOR_Corn.jpg differ
diff --git a/scenes/preview/S1_Cam_Flowers.jpg b/scenes/preview/S1_Cam_Flowers.jpg
new file mode 100644
index 0000000..09ab39b
Binary files /dev/null and b/scenes/preview/S1_Cam_Flowers.jpg differ
diff --git a/scenes/preview/S1_Cam_Louisa_FromP3.jpg b/scenes/preview/S1_Cam_Louisa_FromP3.jpg
new file mode 100644
index 0000000..d339d77
Binary files /dev/null and b/scenes/preview/S1_Cam_Louisa_FromP3.jpg differ
diff --git a/scenes/preview/S1_Cam_P3_Span.jpg b/scenes/preview/S1_Cam_P3_Span.jpg
new file mode 100644
index 0000000..df40d92
Binary files /dev/null and b/scenes/preview/S1_Cam_P3_Span.jpg differ
diff --git a/scenes/preview/S2_Cam_AWOS_Drone.jpg b/scenes/preview/S2_Cam_AWOS_Drone.jpg
new file mode 100644
index 0000000..90286af
Binary files /dev/null and b/scenes/preview/S2_Cam_AWOS_Drone.jpg differ
diff --git a/scenes/preview/S2_Cam_Aerial_SE.jpg b/scenes/preview/S2_Cam_Aerial_SE.jpg
new file mode 100644
index 0000000..2160df2
Binary files /dev/null and b/scenes/preview/S2_Cam_Aerial_SE.jpg differ
diff --git a/scenes/preview/S2_Cam_C850_Drone.jpg b/scenes/preview/S2_Cam_C850_Drone.jpg
new file mode 100644
index 0000000..ad6e8cc
Binary files /dev/null and b/scenes/preview/S2_Cam_C850_Drone.jpg differ
diff --git a/scenes/preview/S2_Cam_CSAPR2_Ground.jpg b/scenes/preview/S2_Cam_CSAPR2_Ground.jpg
new file mode 100644
index 0000000..862d78f
Binary files /dev/null and b/scenes/preview/S2_Cam_CSAPR2_Ground.jpg differ
diff --git a/scenes/preview/S2_Cam_Quad_Drone.jpg b/scenes/preview/S2_Cam_Quad_Drone.jpg
new file mode 100644
index 0000000..40893de
Binary files /dev/null and b/scenes/preview/S2_Cam_Quad_Drone.jpg differ
diff --git a/scenes/preview/S2_Cam_Sign_S2.jpg b/scenes/preview/S2_Cam_Sign_S2.jpg
new file mode 100644
index 0000000..688aedb
Binary files /dev/null and b/scenes/preview/S2_Cam_Sign_S2.jpg differ
diff --git a/scenes/preview/test_instrument_lineup_Cam_Lineup.jpg b/scenes/preview/test_instrument_lineup_Cam_Lineup.jpg
new file mode 100644
index 0000000..5001fed
Binary files /dev/null and b/scenes/preview/test_instrument_lineup_Cam_Lineup.jpg differ
diff --git a/scenes/preview/test_m1_lasso_cumulus_Cam_M1_Aerial_SE.jpg b/scenes/preview/test_m1_lasso_cumulus_Cam_M1_Aerial_SE.jpg
new file mode 100644
index 0000000..c1135da
Binary files /dev/null and b/scenes/preview/test_m1_lasso_cumulus_Cam_M1_Aerial_SE.jpg differ
diff --git a/scenes/preview/test_m1_supercell_Cam_Storm_Sign.jpg b/scenes/preview/test_m1_supercell_Cam_Storm_Sign.jpg
new file mode 100644
index 0000000..360c685
Binary files /dev/null and b/scenes/preview/test_m1_supercell_Cam_Storm_Sign.jpg differ
diff --git a/scenes/preview/test_s1_ucla_cumulus_Cam_Aerial_SW.jpg b/scenes/preview/test_s1_ucla_cumulus_Cam_Aerial_SW.jpg
new file mode 100644
index 0000000..74ac03a
Binary files /dev/null and b/scenes/preview/test_s1_ucla_cumulus_Cam_Aerial_SW.jpg differ
diff --git a/scenes/tests/instrument_lineup.blend b/scenes/tests/instrument_lineup.blend
new file mode 100644
index 0000000..f755ca7
Binary files /dev/null and b/scenes/tests/instrument_lineup.blend differ
diff --git a/scenes/tests/m1_lasso_cumulus.blend b/scenes/tests/m1_lasso_cumulus.blend
new file mode 100644
index 0000000..b706814
Binary files /dev/null and b/scenes/tests/m1_lasso_cumulus.blend differ
diff --git a/scenes/tests/m1_supercell.blend b/scenes/tests/m1_supercell.blend
new file mode 100644
index 0000000..39a123b
Binary files /dev/null and b/scenes/tests/m1_supercell.blend differ
diff --git a/scenes/tests/s1_ucla_cumulus.blend b/scenes/tests/s1_ucla_cumulus.blend
new file mode 100644
index 0000000..554637b
Binary files /dev/null and b/scenes/tests/s1_ucla_cumulus.blend differ
diff --git a/textures/flags/flag_arm.png b/textures/flags/flag_arm.png
new file mode 100644
index 0000000..a1760d8
Binary files /dev/null and b/textures/flags/flag_arm.png differ
diff --git a/textures/flags/flag_isu.png b/textures/flags/flag_isu.png
new file mode 100644
index 0000000..5470ceb
Binary files /dev/null and b/textures/flags/flag_isu.png differ
diff --git a/textures/flags/flag_us.png b/textures/flags/flag_us.png
new file mode 100644
index 0000000..8bd8204
Binary files /dev/null and b/textures/flags/flag_us.png differ
diff --git a/textures/imagery/m1/hf_inner_1m.exr b/textures/imagery/m1/hf_inner_1m.exr
new file mode 100644
index 0000000..8a856c5
Binary files /dev/null and b/textures/imagery/m1/hf_inner_1m.exr differ
diff --git a/textures/imagery/m1/hf_outer_10m.exr b/textures/imagery/m1/hf_outer_10m.exr
new file mode 100644
index 0000000..a7bbd89
Binary files /dev/null and b/textures/imagery/m1/hf_outer_10m.exr differ
diff --git a/textures/imagery/s1_mirf/naip_rgb_0p6m.png b/textures/imagery/s1_mirf/naip_rgb_0p6m.png
new file mode 100644
index 0000000..a73fccc
Binary files /dev/null and b/textures/imagery/s1_mirf/naip_rgb_0p6m.png differ
diff --git a/textures/imagery/s2_kawg/naip_rgb_0p6m_clean.png b/textures/imagery/s2_kawg/naip_rgb_0p6m_clean.png
new file mode 100644
index 0000000..8fb3f3e
Binary files /dev/null and b/textures/imagery/s2_kawg/naip_rgb_0p6m_clean.png differ
diff --git a/textures/imagery/s2_kawg/naip_wide_6m.png b/textures/imagery/s2_kawg/naip_wide_6m.png
new file mode 100644
index 0000000..031bb8f
Binary files /dev/null and b/textures/imagery/s2_kawg/naip_wide_6m.png differ
diff --git a/textures/pbr/Concrete034/Concrete034_2K-JPG_Color.jpg b/textures/pbr/Concrete034/Concrete034_2K-JPG_Color.jpg
new file mode 100644
index 0000000..8bff82f
Binary files /dev/null and b/textures/pbr/Concrete034/Concrete034_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/Concrete034/Concrete034_2K-JPG_NormalGL.jpg b/textures/pbr/Concrete034/Concrete034_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..4d0194c
Binary files /dev/null and b/textures/pbr/Concrete034/Concrete034_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/Concrete034/Concrete034_2K-JPG_Roughness.jpg b/textures/pbr/Concrete034/Concrete034_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..509dbe1
Binary files /dev/null and b/textures/pbr/Concrete034/Concrete034_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_Color.jpg b/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_Color.jpg
new file mode 100644
index 0000000..6f65c56
Binary files /dev/null and b/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_NormalGL.jpg b/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..c6d68ee
Binary files /dev/null and b/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_Roughness.jpg b/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..e073e7d
Binary files /dev/null and b/textures/pbr/CorrugatedSteel005/CorrugatedSteel005_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/Ground037/Ground037_2K-JPG_Color.jpg b/textures/pbr/Ground037/Ground037_2K-JPG_Color.jpg
new file mode 100644
index 0000000..3aa0b16
Binary files /dev/null and b/textures/pbr/Ground037/Ground037_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/Ground037/Ground037_2K-JPG_NormalGL.jpg b/textures/pbr/Ground037/Ground037_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..4de0e07
Binary files /dev/null and b/textures/pbr/Ground037/Ground037_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/Ground037/Ground037_2K-JPG_Roughness.jpg b/textures/pbr/Ground037/Ground037_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..bdace44
Binary files /dev/null and b/textures/pbr/Ground037/Ground037_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/Metal009/Metal009_2K-JPG_Color.jpg b/textures/pbr/Metal009/Metal009_2K-JPG_Color.jpg
new file mode 100644
index 0000000..6afae4b
Binary files /dev/null and b/textures/pbr/Metal009/Metal009_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/Metal009/Metal009_2K-JPG_NormalGL.jpg b/textures/pbr/Metal009/Metal009_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..b42e61e
Binary files /dev/null and b/textures/pbr/Metal009/Metal009_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/Metal009/Metal009_2K-JPG_Roughness.jpg b/textures/pbr/Metal009/Metal009_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..8ea7346
Binary files /dev/null and b/textures/pbr/Metal009/Metal009_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/Metal050A/Metal050A_2K-JPG_Color.jpg b/textures/pbr/Metal050A/Metal050A_2K-JPG_Color.jpg
new file mode 100644
index 0000000..0ebe77a
Binary files /dev/null and b/textures/pbr/Metal050A/Metal050A_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/Metal050A/Metal050A_2K-JPG_NormalGL.jpg b/textures/pbr/Metal050A/Metal050A_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..aa58822
Binary files /dev/null and b/textures/pbr/Metal050A/Metal050A_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/Metal050A/Metal050A_2K-JPG_Roughness.jpg b/textures/pbr/Metal050A/Metal050A_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..3863822
Binary files /dev/null and b/textures/pbr/Metal050A/Metal050A_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/Metal051A/Metal051A_2K-JPG_Color.jpg b/textures/pbr/Metal051A/Metal051A_2K-JPG_Color.jpg
new file mode 100644
index 0000000..007d9a6
Binary files /dev/null and b/textures/pbr/Metal051A/Metal051A_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/Metal051A/Metal051A_2K-JPG_NormalGL.jpg b/textures/pbr/Metal051A/Metal051A_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..2849982
Binary files /dev/null and b/textures/pbr/Metal051A/Metal051A_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/Metal051A/Metal051A_2K-JPG_Roughness.jpg b/textures/pbr/Metal051A/Metal051A_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..47daae6
Binary files /dev/null and b/textures/pbr/Metal051A/Metal051A_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_Color.jpg b/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_Color.jpg
new file mode 100644
index 0000000..24065d6
Binary files /dev/null and b/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_NormalGL.jpg b/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..74d0040
Binary files /dev/null and b/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_Roughness.jpg b/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..1a29b21
Binary files /dev/null and b/textures/pbr/PaintedMetal004/PaintedMetal004_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_Color.jpg b/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_Color.jpg
new file mode 100644
index 0000000..4ab2a09
Binary files /dev/null and b/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_NormalGL.jpg b/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..4ced1a3
Binary files /dev/null and b/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_Roughness.jpg b/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..7473e93
Binary files /dev/null and b/textures/pbr/PaintedMetal006/PaintedMetal006_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/Plastic010/Plastic010_2K-JPG_Color.jpg b/textures/pbr/Plastic010/Plastic010_2K-JPG_Color.jpg
new file mode 100644
index 0000000..6a47602
Binary files /dev/null and b/textures/pbr/Plastic010/Plastic010_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/Plastic010/Plastic010_2K-JPG_NormalGL.jpg b/textures/pbr/Plastic010/Plastic010_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..04a6fa8
Binary files /dev/null and b/textures/pbr/Plastic010/Plastic010_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/Plastic010/Plastic010_2K-JPG_Roughness.jpg b/textures/pbr/Plastic010/Plastic010_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..75a0733
Binary files /dev/null and b/textures/pbr/Plastic010/Plastic010_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/Plastic013A/Plastic013A_2K-JPG_Color.jpg b/textures/pbr/Plastic013A/Plastic013A_2K-JPG_Color.jpg
new file mode 100644
index 0000000..9227a31
Binary files /dev/null and b/textures/pbr/Plastic013A/Plastic013A_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/Plastic013A/Plastic013A_2K-JPG_NormalGL.jpg b/textures/pbr/Plastic013A/Plastic013A_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..068ee99
Binary files /dev/null and b/textures/pbr/Plastic013A/Plastic013A_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/Plastic013A/Plastic013A_2K-JPG_Roughness.jpg b/textures/pbr/Plastic013A/Plastic013A_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..7785539
Binary files /dev/null and b/textures/pbr/Plastic013A/Plastic013A_2K-JPG_Roughness.jpg differ
diff --git a/textures/pbr/Rubber004/Rubber004_2K-JPG_Color.jpg b/textures/pbr/Rubber004/Rubber004_2K-JPG_Color.jpg
new file mode 100644
index 0000000..709ce20
Binary files /dev/null and b/textures/pbr/Rubber004/Rubber004_2K-JPG_Color.jpg differ
diff --git a/textures/pbr/Rubber004/Rubber004_2K-JPG_NormalGL.jpg b/textures/pbr/Rubber004/Rubber004_2K-JPG_NormalGL.jpg
new file mode 100644
index 0000000..66f5593
Binary files /dev/null and b/textures/pbr/Rubber004/Rubber004_2K-JPG_NormalGL.jpg differ
diff --git a/textures/pbr/Rubber004/Rubber004_2K-JPG_Roughness.jpg b/textures/pbr/Rubber004/Rubber004_2K-JPG_Roughness.jpg
new file mode 100644
index 0000000..42b5d96
Binary files /dev/null and b/textures/pbr/Rubber004/Rubber004_2K-JPG_Roughness.jpg differ
diff --git a/textures/signs/derechos_sign_M1.png b/textures/signs/derechos_sign_M1.png
new file mode 100644
index 0000000..cc62734
Binary files /dev/null and b/textures/signs/derechos_sign_M1.png differ
diff --git a/textures/signs/derechos_sign_S1.png b/textures/signs/derechos_sign_S1.png
new file mode 100644
index 0000000..de4ac37
Binary files /dev/null and b/textures/signs/derechos_sign_S1.png differ
diff --git a/textures/signs/derechos_sign_S2.png b/textures/signs/derechos_sign_S2.png
new file mode 100644
index 0000000..3f4e2b0
Binary files /dev/null and b/textures/signs/derechos_sign_S2.png differ
diff --git a/tools/ablib.py b/tools/ablib.py
new file mode 100644
index 0000000..da03048
--- /dev/null
+++ b/tools/ablib.py
@@ -0,0 +1,453 @@
+"""Helpers for building the ablemodels asset library from monolithic scene files.
+
+Run inside Blender (background mode). Environment:
+ ABLE_REPO repository root
+ ABLE_SRC root of the original farm_amf2 working folder (source scenes/textures)
+
+Two ways an embedded collection is moved into a library file:
+ extract() collection keeps world coordinates and is linked back in place
+ (site-specific layers: terrain, fields, buildings at a site)
+ extract_normalized() collection's root object is moved to the origin, written,
+ and the scene gets a collection-instance empty at the old
+ transform (re-usable assets: instruments, containers, vehicles)
+Both are lossless: geometry, materials and modifiers are written unchanged.
+"""
+import bpy, os, re, shutil, json
+from mathutils import Matrix
+
+REPO = os.environ.get('ABLE_REPO', '')
+SRC = os.environ.get('ABLE_SRC', '')
+REPORT = {'written': [], 'linked': [], 'missing': [], 'warnings': [], 'mismatch': []}
+
+
+def log(*a):
+ print('[able]', *a, flush=True)
+
+
+def warn(*a):
+ s = ' '.join(map(str, a)); REPORT['warnings'].append(s); log('WARN', s)
+
+
+# ---------------------------------------------------------------- paths
+def resolve_src(path):
+ if not path:
+ return None
+ p = bpy.path.abspath(path)
+ if os.path.exists(p):
+ return os.path.normpath(p)
+ m = re.search(r'farm_amf2/(.*)$', path.replace('\\', '/'), re.I)
+ if m:
+ cand = os.path.join(SRC, m.group(1))
+ if os.path.exists(cand):
+ return os.path.normpath(cand)
+ return None
+
+
+def texture_dest(src):
+ s = src.replace('\\', '/')
+ fn = os.path.basename(s)
+ m = re.search(r'/_tex/([^/]+)/', s)
+ if m:
+ return os.path.join(REPO, 'textures', 'pbr', m.group(1), fn)
+ if '/mirf_s1/' in s:
+ return os.path.join(REPO, 'textures', 'imagery', 's1_mirf', fn)
+ if '/airport_awg/' in s:
+ return os.path.join(REPO, 'textures', 'imagery', 's2_kawg', fn)
+ if 'flag_' in fn:
+ return os.path.join(REPO, 'textures', 'flags', fn)
+ if '/signs/' in s:
+ return os.path.join(REPO, 'textures', 'signs', fn)
+ if '/lasso/' in s or '/m1_lidar/' in s:
+ return os.path.join(REPO, 'textures', 'imagery', 'm1', fn)
+ return os.path.join(REPO, 'textures', 'misc', fn)
+
+
+def localize_images():
+ """Copy every referenced (unpacked) image into REPO/textures and point at it."""
+ n = 0
+ for img in bpy.data.images:
+ if img.library or img.packed_file or img.source in ('VIEWER', 'GENERATED') or not img.filepath:
+ continue
+ src = resolve_src(img.filepath)
+ if not src:
+ REPORT['missing'].append(('image', img.name, img.filepath))
+ warn('missing image', img.name, img.filepath)
+ continue
+ dst = texture_dest(src)
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
+ if not os.path.exists(dst):
+ shutil.copy2(src, dst)
+ img.filepath = dst
+ n += 1
+ log('localized images', n)
+
+
+def fonts_to_mesh():
+ """Text objects using non-builtin (system) fonts become meshes so files are portable."""
+ dg = bpy.context.evaluated_depsgraph_get()
+ for o in list(bpy.data.objects):
+ if o.library or o.type != 'FONT':
+ continue
+ f = o.data.font
+ if f is None or f.filepath == '' or f.packed_file:
+ continue
+ me = bpy.data.meshes.new_from_object(o.evaluated_get(dg), preserve_all_data_layers=True, depsgraph=dg)
+ name = o.name
+ new = bpy.data.objects.new(name + '_mesh', me)
+ new.parent = o.parent
+ new.matrix_parent_inverse = o.matrix_parent_inverse.copy()
+ new.matrix_basis = o.matrix_basis.copy()
+ new.hide_render = o.hide_render; new.hide_viewport = o.hide_viewport
+ for c in o.users_collection:
+ c.objects.link(new)
+ for ch in o.children:
+ mw = ch.matrix_world.copy(); ch.parent = new; ch.matrix_world = mw
+ bpy.data.objects.remove(o)
+ new.name = name
+ log('font->mesh', name, f.filepath)
+
+
+# ---------------------------------------------------------------- view layer flags
+def _walk_lc(lc, out):
+ out[lc.collection] = (lc.exclude, lc.hide_viewport, lc.collection.hide_render, lc.collection.hide_viewport)
+ for ch in lc.children:
+ _walk_lc(ch, out)
+
+
+def layer_flags(coll):
+ out = {}
+ for sc in bpy.data.scenes:
+ for vl in sc.view_layers:
+ d = {}
+ _walk_lc(vl.layer_collection, d)
+ if coll in d:
+ out[(sc.name, vl.name)] = d[coll]
+ return out
+
+
+def apply_layer_flags(coll, flags):
+ for (scn, vln), (exc, hv, hr, hvp) in flags.items():
+ vl = bpy.data.scenes[scn].view_layers[vln]
+ def find(lc):
+ if lc.collection == coll:
+ return lc
+ for ch in lc.children:
+ r = find(ch)
+ if r: return r
+ lc = find(vl.layer_collection)
+ if lc:
+ lc.exclude = exc
+ if not exc:
+ lc.hide_viewport = hv
+
+
+def is_hidden(flags):
+ return any(v[0] or v[2] for v in flags.values())
+
+
+# ---------------------------------------------------------------- structure helpers
+def parents_of(coll):
+ ps = [c for c in bpy.data.collections if not c.library and coll.name in c.children and c.children[coll.name] == coll]
+ ps += [s.collection for s in bpy.data.scenes if coll.name in s.collection.children and s.collection.children[coll.name] == coll]
+ return ps
+
+
+def descendants(o):
+ return [o] + list(o.children_recursive)
+
+
+def keep_local_objects(coll):
+ """Cameras/lights (and their track targets) stay in the scene file."""
+ keep = set()
+ for o in coll.all_objects:
+ if o.type in ('CAMERA', 'LIGHT'):
+ keep.add(o)
+ for con in o.constraints:
+ t = getattr(con, 'target', None)
+ if t is not None and t in set(coll.all_objects):
+ keep.add(t)
+ return keep
+
+
+def split_out_local(coll, scene_parent=None):
+ keep = keep_local_objects(coll)
+ if not keep:
+ return None
+ name = coll.name + '_cams'
+ lc = bpy.data.collections.get(name) or bpy.data.collections.new(name)
+ (scene_parent or bpy.context.scene.collection).children.link(lc) if lc.name not in (scene_parent or bpy.context.scene.collection).children else None
+ for o in keep:
+ for c in list(o.users_collection):
+ if c == coll or c in set(coll.children_recursive):
+ c.objects.unlink(o)
+ if lc not in o.users_collection:
+ lc.objects.link(o)
+ log('kept local', name, [o.name for o in keep])
+ return lc
+
+
+def make_root(coll, root_name, at_obj=None):
+ """Give a collection of loose objects a single root empty (transforms preserved)."""
+ roots = [o for o in coll.objects if o.parent is None or o.parent not in set(coll.all_objects)]
+ loc = (at_obj.matrix_world.translation.copy() if at_obj else roots[0].matrix_world.translation.copy())
+ e = bpy.data.objects.new(root_name, None)
+ e.empty_display_size = 0.5
+ coll.objects.link(e)
+ e.matrix_world = Matrix.Translation(loc)
+ bpy.context.view_layer.update()
+ for o in roots:
+ mw = o.matrix_world.copy(); o.parent = e; o.matrix_world = mw
+ bpy.context.view_layer.update()
+ return e
+
+
+def collection_from_roots(parent_coll, root_names, new_name_fn, dest_parent=None):
+ """Move each root (+ all descendants) out of parent_coll into its own new collection,
+ created under dest_parent (default: parent_coll)."""
+ out = []
+ dest_parent = dest_parent or parent_coll
+ for rn in root_names:
+ r = bpy.data.objects[rn]
+ nc = bpy.data.collections.new(new_name_fn(rn))
+ dest_parent.children.link(nc)
+ for o in descendants(r):
+ for c in list(o.users_collection):
+ if c == parent_coll:
+ c.objects.unlink(o)
+ nc.objects.link(o)
+ out.append((nc, r))
+ return out
+
+
+# ---------------------------------------------------------------- write / link
+def write_lib(relpath, ids, mark_asset=True):
+ path = os.path.join(REPO, relpath)
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ for i in ids:
+ if mark_asset and isinstance(i, bpy.types.Collection) and i.asset_data is None:
+ try:
+ i.asset_mark()
+ except Exception as e:
+ warn('asset_mark failed', i.name, e)
+ bpy.data.libraries.write(path, set(ids), path_remap='RELATIVE_ALL', fake_user=True, compress=True)
+ REPORT['written'].append((relpath, [i.name for i in ids], os.path.getsize(path)))
+ log('wrote', relpath, round(os.path.getsize(path) / 1e6, 2), 'MB', [i.name for i in ids])
+ return path
+
+
+def link_colls(relpath, names):
+ path = os.path.join(REPO, relpath)
+ with bpy.data.libraries.load(path, link=True, relative=True) as (src, dst):
+ missing = [n for n in names if n not in src.collections]
+ if missing:
+ raise RuntimeError(f'{relpath} lacks {missing}')
+ dst.collections = list(names)
+ REPORT['linked'].append((relpath, names))
+ return dict(zip(names, dst.collections))
+
+
+def delete_local(colls, keep=()):
+ allc = set()
+ for c in colls:
+ allc.add(c); allc |= set(c.children_recursive)
+ objs = set()
+ for c in allc:
+ objs |= set(o for o in c.all_objects if not o.library)
+ for o in list(objs):
+ if o in keep:
+ continue
+ other = [c for c in o.users_collection if c not in allc]
+ if other:
+ warn('object also in other collections, kept local:', o.name, [c.name for c in other])
+ continue
+ bpy.data.objects.remove(o, do_unlink=True)
+ for c in sorted(allc, key=lambda c: -len(c.children_recursive)):
+ if not c.library and c.name in bpy.data.collections:
+ bpy.data.collections.remove(c)
+
+
+def purge():
+ for _ in range(3):
+ bpy.data.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
+
+
+def extract(relpath, names):
+ """World-coordinate extraction: write collections, link them back in place."""
+ colls = [bpy.data.collections[n] for n in names]
+ for c in colls:
+ split_out_local(c)
+ flags = {c.name: layer_flags(c) for c in colls}
+ write_lib(relpath, colls)
+ linked = link_colls(relpath, names)
+ for c in colls:
+ c.user_remap(linked[c.name])
+ delete_local(colls)
+ for n, l in linked.items():
+ apply_layer_flags(l, flags[n])
+ return linked
+
+
+def extract_normalized(relpath, specs):
+ """specs: list of (collection_name, root_object_name). Root goes to origin in the library;
+ the scene gets an instance empty with the root's old world matrix."""
+ items = []
+ for cn, rn in specs:
+ c = bpy.data.collections[cn]; r = bpy.data.objects[rn]
+ if r.parent is not None:
+ warn('root has parent', rn, r.parent.name)
+ W = r.matrix_world.copy()
+ flags = layer_flags(c)
+ parents = parents_of(c)
+ hide_r, hide_v = r.hide_render, r.hide_viewport
+ r.matrix_world = Matrix.Identity(4)
+ items.append((c, r, W, flags, parents, hide_r, hide_v))
+ bpy.context.view_layer.update()
+ write_lib(relpath, [i[0] for i in items])
+ linked = link_colls(relpath, [i[0].name for i in items])
+ for c, r, W, flags, parents, hide_r, hide_v in items:
+ l = linked[c.name]
+ # existing instancers of this collection expected the root at its old place
+ for o in bpy.data.objects:
+ if not o.library and o.instance_type == 'COLLECTION' and o.instance_collection == c:
+ o.instance_collection = l
+ o.matrix_world = o.matrix_world @ W
+ placed = [p for p in parents if not p.name.startswith('_')]
+ for p in parents:
+ p.children.unlink(c)
+ if placed:
+ e = bpy.data.objects.new(c.name + '_inst', None)
+ e.instance_type = 'COLLECTION'; e.instance_collection = l
+ e.empty_display_size = 1.0
+ e.matrix_world = W
+ e.hide_render = hide_r or is_hidden(flags)
+ e.hide_viewport = hide_v or is_hidden(flags)
+ for p in placed:
+ p.objects.link(e)
+ else:
+ # prototype libraries (_AssetLib etc.) keep the linked collection as a child
+ for p in parents:
+ p.children.link(l)
+ apply_layer_flags(l, flags)
+ c.user_remap(l)
+ delete_local([i[0] for i in items])
+ return linked
+
+
+# ---------------------------------------------------------------- comparison / relink
+def _base(n):
+ return re.sub(r'\.\d{3}$', '', n)
+
+
+def world(o):
+ """matrix_world from the parent chain (linked objects outside a scene are not evaluated)."""
+ if o.parent is None:
+ return o.matrix_basis.copy()
+ return world(o.parent) @ o.matrix_parent_inverse @ o.matrix_basis
+
+
+def signature(coll, root=None):
+ """Name-independent fingerprint: per object type, vertex count and the mesh's
+ bounds relative to the root (object names get renumbered between files, and
+ bound_box is not evaluated for linked data, so vertices are read directly)."""
+ import numpy as np
+ inv = world(root).inverted() if root else Matrix.Identity(4)
+ sig = []
+ for o in coll.all_objects:
+ M = inv @ world(o)
+ if o.type == 'MESH' and len(o.data.vertices):
+ co = np.empty(len(o.data.vertices) * 3); o.data.vertices.foreach_get('co', co)
+ co = co.reshape(-1, 3)
+ m = np.array(M)
+ w = co @ m[:3, :3].T + m[:3, 3]
+ b = tuple(np.round(w.min(0), 2)) + tuple(np.round(w.max(0), 2))
+ sig.append(('MESH', len(co)) + tuple(float(x) for x in b))
+ else:
+ t = M.translation
+ sig.append((o.type, 0, round(t.x, 2), round(t.y, 2), round(t.z, 2)))
+ return sorted(sig)
+
+
+def relink(relpath, mapping, roots=None):
+ """mapping {local_collection: library_collection}. Remaps only collections whose
+ signature matches the library copy; returns the names that did not match."""
+ linked = link_colls(relpath, list(mapping.values()))
+ bad = []
+ for ln, libn in mapping.items():
+ c = bpy.data.collections[ln]; l = linked[libn]
+ r_loc = bpy.data.objects.get(roots[ln]) if roots else None
+ r_lib = None
+ if roots:
+ r_lib = next((o for o in l.all_objects if _base(o.name) == _base(roots[ln])), None)
+ if signature(c, r_loc) != signature(l, r_lib):
+ bad.append(ln); REPORT['mismatch'].append((ln, relpath, libn))
+ warn('signature mismatch', ln, 'vs', relpath, libn)
+ continue
+ flags = layer_flags(c)
+ c.user_remap(l)
+ delete_local([c])
+ apply_layer_flags(l, flags)
+ log('relinked', ln, '->', relpath, libn)
+ for l in linked.values():
+ if l.users == 0 or (l.users == 1 and l.use_fake_user):
+ pass
+ return bad
+
+
+def finish(scene_relpath, report_relpath=None):
+ purge()
+ path = os.path.join(REPO, scene_relpath)
+ bpy.ops.file.make_paths_relative()
+ bpy.ops.wm.save_as_mainfile(filepath=path, compress=True, relative_remap=True)
+ # verify that every library and image resolves
+ for lib in bpy.data.libraries:
+ if not os.path.exists(bpy.path.abspath(lib.filepath)):
+ REPORT['missing'].append(('library', lib.filepath))
+ for img in bpy.data.images:
+ if img.packed_file or img.source in ('VIEWER', 'GENERATED') or not img.filepath:
+ continue
+ if not os.path.exists(bpy.path.abspath(img.filepath, library=img.library)):
+ REPORT['missing'].append(('image', img.name, img.filepath))
+ REPORT['scene'] = (scene_relpath, os.path.getsize(path))
+ if report_relpath:
+ with open(os.path.join(REPO, report_relpath), 'w') as f:
+ json.dump(REPORT, f, indent=1, default=str)
+ log('saved', scene_relpath, round(os.path.getsize(path) / 1e6, 2), 'MB; missing:', REPORT['missing'])
+
+
+def relink_normalized(relpath, cn, rn, libcn=None):
+ """Replace a scene-placed collection by an instance of an existing library asset,
+ if the two match (signature relative to their roots). Returns True on success."""
+ libcn = libcn or cn
+ c = bpy.data.collections[cn]; r = bpy.data.objects[rn]
+ l = link_colls(relpath, [libcn])[libcn]
+ r_lib = next((o for o in l.all_objects if _base(o.name) == _base(rn)), None)
+ if r_lib is None or signature(c, r) != signature(l, r_lib):
+ REPORT['mismatch'].append((cn, relpath, libcn)); warn('normalized mismatch', cn, relpath, libcn)
+ return False
+ W = r.matrix_world @ r_lib.matrix_world.inverted()
+ flags = layer_flags(c); parents = parents_of(c)
+ for p in parents:
+ p.children.unlink(c)
+ placed = [p for p in parents if not p.name.startswith('_')]
+ if placed:
+ e = bpy.data.objects.new(libcn + '_inst', None)
+ e.instance_type = 'COLLECTION'; e.instance_collection = l
+ e.matrix_world = W
+ e.hide_render = r.hide_render or is_hidden(flags)
+ e.hide_viewport = r.hide_viewport or is_hidden(flags)
+ for p in placed:
+ p.objects.link(e)
+ for o in bpy.data.objects:
+ if not o.library and o.instance_type == 'COLLECTION' and o.instance_collection == c:
+ o.instance_collection = l
+ o.matrix_world = o.matrix_world @ r.matrix_world @ r_lib.matrix_world.inverted()
+ c.user_remap(l)
+ delete_local([c])
+ log('relinked (normalized)', cn, '->', relpath, libcn)
+ return True
+
+
+def rename_collections(mapping):
+ for a, b in mapping.items():
+ if a in bpy.data.collections:
+ bpy.data.collections[a].name = b
diff --git a/tools/audit_paths.py b/tools/audit_paths.py
new file mode 100644
index 0000000..1cd5ccb
--- /dev/null
+++ b/tools/audit_paths.py
@@ -0,0 +1,70 @@
+"""Check (and with --fix, repair) external paths in one .blend so the repo is self-contained.
+blender -b file.blend --python tools/audit_paths.py -- report.json [--fix]
+"""
+import bpy, os, sys, json
+args = sys.argv[sys.argv.index('--') + 1:]
+fix = '--fix' in args
+REPO = os.environ['ABLE_REPO']
+here = bpy.data.filepath
+rep = {'file': os.path.relpath(here, REPO), 'problems': [], 'fixed': []}
+
+
+def inside_repo(p):
+ return os.path.abspath(p).startswith(REPO + os.sep)
+
+
+def check(kind, idname, raw, lib=None, packed=False):
+ if not raw or raw == '':
+ return
+ ab = bpy.path.abspath(raw, library=lib)
+ bad = []
+ if not raw.startswith('//'):
+ bad.append('absolute')
+ if not inside_repo(ab):
+ bad.append('outside-repo')
+ if not packed and not os.path.exists(ab):
+ bad.append('missing')
+ if bad:
+ rep['problems'].append((kind, idname, raw, bad, packed))
+ return bad
+
+
+changed = False
+for l in bpy.data.libraries:
+ check('library', l.name, l.filepath)
+for im in bpy.data.images:
+ if im.library or im.source in ('VIEWER', 'GENERATED') or not im.filepath:
+ continue
+ bad = check('image', im.name, im.filepath, packed=bool(im.packed_file))
+ if bad and im.packed_file and fix:
+ import glob
+ base = os.path.basename(im.filepath)
+ hits = glob.glob(os.path.join(REPO, 'textures', '**', base), recursive=True)
+ im.filepath_raw = bpy.path.relpath(hits[0]) if hits else '//' + base
+ rep['fixed'].append(('packed image path', im.name, im.filepath_raw)); changed = True
+for v in bpy.data.volumes:
+ if not v.library:
+ check('volume', v.name, v.filepath, packed=bool(v.packed_file))
+for f in bpy.data.fonts:
+ if not f.library:
+ check('font', f.name, f.filepath, packed=bool(f.packed_file))
+for s in bpy.data.scenes:
+ if s.library:
+ continue
+ fp = s.render.filepath
+ if fp and not fp.startswith('//') and fp not in ('/tmp/', '/tmp'):
+ rep['problems'].append(('render.filepath', s.name, fp, ['absolute'], False))
+ if fix:
+ s.render.filepath = '//renders/'; rep['fixed'].append(('render.filepath', s.name)); changed = True
+for o in bpy.data.objects:
+ if o.library:
+ continue
+ for m in o.modifiers:
+ bd = getattr(m, 'bake_directory', '')
+ if bd:
+ rep['problems'].append(('bake_directory', o.name, bd, ['bake'], False))
+ if m.type == 'MESH_SEQUENCE_CACHE' and m.cache_file:
+ check('cachefile', m.cache_file.name, m.cache_file.filepath)
+if fix and changed:
+ bpy.ops.wm.save_mainfile(compress=True)
+json.dump(rep, open(args[0], 'w'), indent=1)
diff --git a/tools/build_clouds.py b/tools/build_clouds.py
new file mode 100644
index 0000000..537fc7f
--- /dev/null
+++ b/tools/build_clouds.py
@@ -0,0 +1,63 @@
+"""Build a cloud asset file: mature snapshots of one simulation, each as its own collection.
+blender -b --python tools/build_clouds.py --
+spec: {"set": "cm1_supercell", "template": ["CM1_Storm"] | "prefix:LASSO_1740_",
+ "normalize": true, "snapshots": [["name", "relative/src.vdb"], ...]}
+VDBs larger than 50 MB are rewritten with 16-bit (half-float) storage; nothing is resampled.
+"""
+import bpy, os, sys, json, shutil
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import ablib as A
+spec = json.load(open(sys.argv[sys.argv.index('--') + 1]))
+D = bpy.data
+outdir = os.path.join(A.REPO, 'clouds', spec['set'])
+os.makedirs(outdir, exist_ok=True)
+
+t = spec['template']
+if isinstance(t, str) and t.startswith('prefix:'):
+ tmpl = sorted([o for o in D.objects if o.type == 'VOLUME' and o.name.startswith(t[7:])], key=lambda o: o.name)
+else:
+ tmpl = [D.objects[n] for n in t]
+A.log('template objects', [o.name for o in tmpl])
+
+
+def place_vdb(src_rel):
+ src = os.path.join(A.SRC, src_rel)
+ dst = os.path.join(outdir, os.path.basename(src))
+ if os.path.exists(dst):
+ return dst
+ if os.path.getsize(src) > 50e6:
+ import openvdb as vdb
+ grids, meta = vdb.readAll(src)
+ for g in grids:
+ g.saveFloatAsHalf = True
+ meta['ablemodels_note'] = 'rewritten with half-float storage from ' + os.path.basename(src)
+ vdb.write(dst, grids, meta)
+ A.log('half-float', os.path.basename(src), round(os.path.getsize(src) / 1e6), '->', round(os.path.getsize(dst) / 1e6), 'MB')
+ else:
+ shutil.copy2(src, dst)
+ return dst
+
+
+colls = []
+for name, rel in spec['snapshots']:
+ path = place_vdb(rel)
+ vol = tmpl[0].data.copy()
+ vol.name = name
+ vol.filepath = path
+ vol.is_sequence = False
+ c = D.collections.new('CLOUD_' + name)
+ bpy.context.scene.collection.children.link(c)
+ for o in tmpl:
+ n = o.copy(); n.data = vol; n.animation_data_clear()
+ n.name = f'{name}_{o.name}' if len(tmpl) > 1 else name
+ n.parent = None
+ n.matrix_world = o.matrix_world.copy()
+ if spec.get('normalize') and len(tmpl) == 1:
+ n['source_location'] = list(o.matrix_world.translation)
+ n.location = (0, 0, 0)
+ c.objects.link(n)
+ c['source_vdb'] = rel
+ c['source_scene'] = os.path.basename(D.filepath)
+ colls.append(c)
+A.write_lib(f'clouds/{spec["set"]}/{spec["set"]}.blend', colls)
+json.dump(A.REPORT, open(os.path.join(A.REPO, 'tools/reports', f'clouds_{spec["set"]}.json'), 'w'), indent=1, default=str)
diff --git a/tools/build_m1.py b/tools/build_m1.py
new file mode 100644
index 0000000..b717063
--- /dev/null
+++ b/tools/build_m1.py
@@ -0,0 +1,61 @@
+"""Rebuild the M1 (main site, farm) scene from farm_v5.blend with linked assets.
+blender -b /farm_v5.blend --python tools/build_m1.py
+"""
+import bpy, os, sys
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import ablib as A
+
+D = bpy.data
+os.makedirs(os.path.join(A.REPO, 'scenes/M1/layers'), exist_ok=True)
+bpy.ops.wm.save_as_mainfile(filepath=os.path.join(A.REPO, 'scenes/M1/M1.blend'), relative_remap=True)
+A.fonts_to_mesh()
+A.localize_images()
+
+# --- plants (prototype collections used by geometry-node scatterers)
+A.extract('plants/crops.blend', ['CornVariants', 'SoyVariants'])
+A.extract('plants/grasses.blend', ['GrassVariants', 'TallGrassVariants'])
+A.extract('plants/trees.blend', ['TreeVariants', 'TV_Decid', 'TV_Spruce', 'TV_Far', 'TreeParts'])
+
+# --- ground first: other layers conform to the Ground terrain object
+A.extract('scenes/M1/layers/m1_ground.blend', ['Ground'])
+
+# --- instruments: one file per instrument, root at origin
+inst = [c.name for c in D.collections['Instruments_v4'].children if c.name.startswith('INST_')]
+for cn in inst:
+ name = cn[5:]
+ root = next(o for o in D.collections[cn].objects if o.name.startswith(name + '_root'))
+ specs = [(cn, root.name)]
+ if name == 'ECOR' and 'INST_ECOR_4m' in D.collections:
+ r4 = next(o for o in D.collections['INST_ECOR_4m'].objects if o.name.startswith('ECOR_root'))
+ specs.append(('INST_ECOR_4m', r4.name))
+ A.extract_normalized(f'instruments/{name}/{name}.blend', specs)
+
+# --- containers: each AMF2 container/shelter becomes its own asset
+amf = D.collections['AMF2']
+A.split_out_local(amf)
+roots = [o.name for o in amf.objects if o.type == 'EMPTY' and o.parent is None and
+ (o.name.startswith('AMF2_Cont_') or o.name in ('AOS_Platform', 'AMF2_OfficeShelter', 'AMF2_RentalContainer'))]
+def cname(r):
+ return 'CONT_' + r.replace('AMF2_Cont_', '').replace('AMF2_', '')
+cont = D.collections.new('Containers'); bpy.context.scene.collection.children.link(cont)
+pairs = A.collection_from_roots(amf, roots, cname, cont)
+A.extract_normalized('containers/amf2_containers.blend', [(c.name, r.name) for c, r in pairs])
+
+# --- vehicles: each farm vehicle is an asset at the origin
+veh = D.collections['Vehicles']
+vroots = [o.name for o in veh.objects if o.parent is None]
+pairs = A.collection_from_roots(veh, vroots, lambda r: 'VEH_' + r)
+A.extract_normalized('vehicles/farm_vehicles.blend', [(c.name, r.name) for c, r in pairs])
+
+# --- buildings / structures
+A.extract('buildings/m1_farmstead.blend', ['Farmstead'])
+fp = D.collections['Flagpole']
+r = A.make_root(fp, 'Flagpole_root', D.objects['Flagpole_Base'])
+A.extract_normalized('buildings/flagpole.blend', [('Flagpole', 'Flagpole_root')])
+A.extract('buildings/signs/sign_M1.blend', ['SIGN_M1'])
+
+# --- remaining site layers
+A.extract('scenes/M1/layers/m1_fields.blend', ['Fields'])
+A.extract('scenes/M1/layers/m1_amf2_site.blend', ['AMF2'])
+
+A.finish('scenes/M1/M1.blend', 'tools/reports/build_m1.json')
diff --git a/tools/build_s1.py b/tools/build_s1.py
new file mode 100644
index 0000000..be004f3
--- /dev/null
+++ b/tools/build_s1.py
@@ -0,0 +1,46 @@
+"""Rebuild S1 (Muscatine Island Research Farm) from mirf_s1.blend with linked assets."""
+import bpy, os, sys
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import ablib as A
+D = bpy.data
+os.makedirs(os.path.join(A.REPO, 'scenes/S1/layers'), exist_ok=True)
+bpy.ops.wm.save_as_mainfile(filepath=os.path.join(A.REPO, 'scenes/S1/S1.blend'), relative_remap=True)
+A.fonts_to_mesh()
+A.localize_images()
+
+# --- plants: reuse M1 crops if identical, otherwise keep S1's own versions
+bad = A.relink('plants/crops.blend', {'CornVariants': 'CornVariants', 'SoyVariants': 'SoyVariants'})
+if bad:
+ A.rename_collections({n: n + '_S1' for n in bad})
+ A.extract('plants/crops_s1.blend', [n + '_S1' for n in bad])
+# high-detail tree set used at S1/S2 (different meshes from the M1 set)
+A.rename_collections({'TV_Spruce': 'TV_Spruce_HD', 'TV_Decid': 'TV_Decid_HD', 'TV_Far': 'TV_Far_HD',
+ 'TreeParts': 'TreeParts_HD', 'TV_DecidBaked': 'TV_DecidBaked_HD'})
+A.extract('plants/trees_hd.blend', ['TV_Spruce_HD', 'TV_Decid_HD', 'TV_Far_HD', 'TreeParts_HD', 'TV_DecidBaked_HD'])
+A.extract('plants/native_flowers.blend', ['NativeFlowerVariants'])
+
+A.extract('scenes/S1/layers/s1_terrain.blend', ['Terrain'])
+
+# --- instruments: ECOR/SEBS prototypes (instanced by S1_Site empties)
+for cn, rn, lib, libcns in [('INST_ECOR', 'ECOR_root', 'instruments/ECOR/ECOR.blend', ['INST_ECOR_4m', 'INST_ECOR']),
+ ('INST_SEBS', 'SEBS_root', 'instruments/SEBS/SEBS.blend', ['INST_SEBS'])]:
+ done = False
+ for libcn in libcns:
+ bad = A.relink(lib, {cn: libcn}, roots={cn: rn})
+ if not bad:
+ done = True; break
+ if not done:
+ new = cn + '_S1'
+ A.rename_collections({cn: new})
+ A.extract_normalized(lib.replace('.blend', '_S1.blend'), [(new, rn)])
+
+# --- structures
+A.extract_normalized('buildings/center_pivots.blend',
+ [('Pivot_P1', 'Pivot_P1_root'), ('Pivot_P2', 'Pivot_P2_root'), ('Pivot_P3', 'Pivot_P3_root')])
+A.extract('buildings/louisa_station.blend', ['LOUISA_Station', 'LOUISA_TreePoints'])
+A.rename_collections({'Buildings': 'S1_MIRF_Buildings'})
+A.extract('buildings/s1_mirf_buildings.blend', ['S1_MIRF_Buildings'])
+A.extract('buildings/signs/sign_S1.blend', ['SIGN_S1'])
+
+A.extract('scenes/S1/layers/s1_vegetation.blend', ['Vegetation'])
+A.finish('scenes/S1/S1.blend', 'tools/reports/build_s1.json')
diff --git a/tools/build_s2.py b/tools/build_s2.py
new file mode 100644
index 0000000..0961c8c
--- /dev/null
+++ b/tools/build_s2.py
@@ -0,0 +1,46 @@
+"""Rebuild S2 (Washington Municipal Airport, KAWG) from airport_awg.blend with linked assets."""
+import bpy, os, sys
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import ablib as A
+D = bpy.data
+os.makedirs(os.path.join(A.REPO, 'scenes/S2/layers'), exist_ok=True)
+bpy.ops.wm.save_as_mainfile(filepath=os.path.join(A.REPO, 'scenes/S2/S2.blend'), relative_remap=True)
+A.fonts_to_mesh()
+A.localize_images()
+
+# --- plants
+bad = A.relink('plants/crops.blend', {'CornVariants': 'CornVariants', 'SoyVariants': 'SoyVariants'})
+if bad and os.path.exists(os.path.join(A.REPO, 'plants/crops_s1.blend')):
+ bad = A.relink('plants/crops_s1.blend', {n: n + '_S1' for n in bad})
+if bad:
+ A.rename_collections({n: n + '_S2' for n in bad})
+ A.extract('plants/crops_s2.blend', [n + '_S2' for n in bad])
+bad = A.relink('plants/grasses.blend', {'GrassVariants': 'GrassVariants', 'TallGrassVariants': 'TallGrassVariants'})
+if bad:
+ A.rename_collections({n: n + '_S2' for n in bad})
+ A.extract('plants/grasses_s2.blend', [n + '_S2' for n in bad])
+bad = A.relink('plants/trees_hd.blend', {'TV_DecidBaked': 'TV_DecidBaked_HD'})
+if bad:
+ A.rename_collections({'TV_DecidBaked': 'TV_DecidBaked_S2'})
+ A.extract('plants/trees_s2.blend', ['TV_DecidBaked_S2'])
+
+A.extract('scenes/S2/layers/s2_terrain.blend', ['Terrain'])
+
+# --- instruments, aircraft, SACR container
+A.extract_normalized('instruments/CSAPR2/CSAPR2.blend', [('CSAPR2', 'CSAPR2_root')])
+A.rename_collections({'AWOS_KAWG': 'INST_AWOS'})
+A.extract_normalized('instruments/AWOS/AWOS.blend', [('INST_AWOS', 'AWOS_root')])
+A.extract_normalized('vehicles/c850_arm.blend', [('C850_ARM', 'C850_root')])
+site = D.collections['S1_Site']
+pairs = A.collection_from_roots(site, ['AMF2_Cont_7_SACR'], lambda r: 'CONT_7_SACR')
+if not A.relink_normalized('containers/amf2_containers.blend', 'CONT_7_SACR', 'AMF2_Cont_7_SACR'):
+ A.rename_collections({'CONT_7_SACR': 'CONT_7_SACR_S2'})
+ A.extract_normalized('containers/sacr_container_s2.blend', [('CONT_7_SACR_S2', 'AMF2_Cont_7_SACR')])
+
+# --- buildings and site infrastructure
+A.rename_collections({'Buildings': 'S2_KAWG_Buildings'})
+A.extract('buildings/s2_kawg_buildings.blend', ['S2_KAWG_Buildings'])
+A.extract('buildings/signs/sign_S2.blend', ['SIGN_S2'])
+A.extract('scenes/S2/layers/s2_power_roads.blend', ['S1_Power_Roads'])
+A.extract('scenes/S2/layers/s2_vegetation.blend', ['Vegetation'])
+A.finish('scenes/S2/S2.blend', 'tools/reports/build_s2.json')
diff --git a/tools/build_tests.py b/tools/build_tests.py
new file mode 100644
index 0000000..16b6eac
--- /dev/null
+++ b/tools/build_tests.py
@@ -0,0 +1,102 @@
+"""Build the test scenes in scenes/tests/.
+blender -b --python tools/build_tests.py --
+"""
+import bpy, os, sys, glob
+from mathutils import Vector, Matrix
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import ablib as A
+name = sys.argv[sys.argv.index('--') + 1]
+D = bpy.data
+out = os.path.join(A.REPO, 'scenes/tests', name + '.blend')
+os.makedirs(os.path.dirname(out), exist_ok=True)
+bpy.ops.wm.save_as_mainfile(filepath=out, relative_remap=True) # paths now relative to tests/
+
+
+def link(lib, colls=(), objs=(), worlds=()):
+ with D.libraries.load(os.path.join(A.REPO, lib), link=True, relative=True) as (s, d):
+ d.collections = list(colls); d.objects = list(objs); d.worlds = list(worlds)
+ return d
+
+
+def instance(coll, matrix=Matrix.Identity(4), parent=None, name=None):
+ e = D.objects.new(name or coll.name + '_inst', None)
+ e.instance_type = 'COLLECTION'; e.instance_collection = coll; e.matrix_world = matrix
+ (parent or bpy.context.scene.collection).objects.link(e)
+ return e
+
+
+def camera_from(src_blend, cam_name, new_name):
+ """Copy a camera's evaluated transform and lens out of one of the original working files
+ (evaluated in a separate Blender process so parents/constraints are applied)."""
+ import subprocess, json, tempfile
+ tmp = tempfile.mktemp(suffix='.json')
+ code = ("import bpy,json;o=bpy.data.objects[%r];dg=bpy.context.evaluated_depsgraph_get();e=o.evaluated_get(dg);"
+ "json.dump({'m':[list(r) for r in e.matrix_world],'lens':o.data.lens,'clip':o.data.clip_end,"
+ "'sw':o.data.sensor_width},open(%r,'w'))") % (cam_name, tmp)
+ subprocess.run([bpy.app.binary_path, '-b', src_blend, '--python-expr', code], check=True,
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ d = json.load(open(tmp))
+ cd = D.cameras.new(new_name); cd.lens = d['lens']; cd.clip_end = d['clip']; cd.sensor_width = d['sw']
+ cam = D.objects.new(new_name, cd); cam.matrix_world = Matrix(d['m'])
+ bpy.context.scene.collection.objects.link(cam)
+ return cam
+
+
+clouds = D.collections.get('Clouds') or D.collections.new('Clouds')
+if name != 'instrument_lineup' and 'Clouds' not in bpy.context.scene.collection.children:
+ bpy.context.scene.collection.children.link(clouds)
+
+if name == 'm1_supercell':
+ c = link('clouds/cm1_supercell/cm1_supercell.blend', ['CLOUD_supercell_c100_t25']).collections[0]
+ # same storm placement as farm_v5_storm_studio.blend
+ instance(c, Matrix.Translation((-34948.7, -11251.7, 0.0)), clouds)
+ cam = camera_from(os.path.join(A.SRC, 'farm_v5_storm_studio.blend'), 'Cam_Storm_Sign', 'Cam_Storm_Sign')
+ bpy.context.scene.camera = cam
+
+elif name == 'm1_lasso_cumulus':
+ c = link('clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend', ['CLOUD_lasso8_2040']).collections[0]
+ instance(c, Matrix.Identity(4), clouds)
+ bpy.context.scene.camera = D.objects['Cam_M1_Aerial_SE']
+
+elif name == 's1_ucla_cumulus':
+ c = link('clouds/ucla_cumulus/ucla_cumulus.blend', ['CLOUD_ucla_t6600']).collections[0]
+ instance(c, Matrix.Identity(4), clouds)
+ bpy.context.scene.camera = D.objects['Cam_Aerial_SW']
+
+elif name == 'instrument_lineup':
+ for o in list(D.objects):
+ D.objects.remove(o)
+ d = link('scenes/M1/M1.blend', objs=['Sun'], worlds=['World'])
+ sc = bpy.context.scene
+ sc.world = d.worlds[0]; sc.collection.objects.link(d.objects[0])
+ row = D.collections.new('Instruments'); sc.collection.children.link(row)
+ libs = sorted(glob.glob(os.path.join(A.REPO, 'instruments/*/*.blend')))
+ x = 0.0
+ for lib in libs:
+ rel = os.path.relpath(lib, A.REPO)
+ with D.libraries.load(lib, link=True, relative=True) as (s, dd):
+ dd.collections = [n for n in s.collections if n.startswith('INST_')]
+ for c in dd.collections:
+ w = 14.0 if 'CSAPR2' in c.name else 4.0
+ instance(c, Matrix.Translation((x + w / 2, 0, 0)), row)
+ x += w
+ bpy.ops.mesh.primitive_plane_add(size=1, location=(x / 2, 0, 0))
+ g = bpy.context.active_object; g.name = 'Ground'; g.scale = (x + 600, 600, 1)
+ m = D.materials.new('Gravel'); m.use_nodes = True
+ m.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = (0.3, 0.29, 0.26, 1)
+ g.data.materials.append(m)
+ cd = D.cameras.new('Cam_Lineup'); cd.lens = 35
+ cam = D.objects.new('Cam_Lineup', cd); sc.collection.objects.link(cam)
+ cam.location = (x / 2, -x * 0.95, 10.0)
+ cam.rotation_euler = (Vector((x / 2, 0, 2.5)) - cam.location).to_track_quat('-Z', 'Y').to_euler()
+ sc.camera = cam
+ sc.render.engine = 'CYCLES'; sc.cycles.samples = 64
+ sc.render.resolution_x, sc.render.resolution_y = 1920, 1080
+
+if name != 'instrument_lineup':
+ # light-path settings of the original cloud scenes (multiple scattering in the clouds)
+ cy = bpy.context.scene.cycles
+ cy.volume_bounces = 64; cy.max_bounces = 68; cy.samples = 64
+bpy.ops.file.make_paths_relative()
+bpy.ops.wm.save_as_mainfile(filepath=out, compress=True, relative_remap=True)
+print('TEST_SAVED', out, os.path.getsize(out))
diff --git a/tools/cloud_specs/cm1_bomex_cumulus.json b/tools/cloud_specs/cm1_bomex_cumulus.json
new file mode 100644
index 0000000..41c0509
--- /dev/null
+++ b/tools/cloud_specs/cm1_bomex_cumulus.json
@@ -0,0 +1,4 @@
+{"set": "cm1_bomex_cumulus", "source": "farm_v5_cu_cm1.blend", "template": ["CM1_Storm"], "normalize": true,
+ "snapshots": [["bomex_t240", "cm1/runs/bomex50/bomex50_t240_x5.vdb"],
+ ["bomex_t300", "cm1/runs/bomex50/bomex50_t300_x5.vdb"],
+ ["bomex_t360", "cm1/runs/bomex50/bomex50_t360_x5.vdb"]]}
diff --git a/tools/cloud_specs/cm1_supercell.json b/tools/cloud_specs/cm1_supercell.json
new file mode 100644
index 0000000..0119ad2
--- /dev/null
+++ b/tools/cloud_specs/cm1_supercell.json
@@ -0,0 +1,6 @@
+{"set": "cm1_supercell", "source": "farm_v5_storm_studio.blend", "template": ["CM1_Storm"], "normalize": true,
+ "snapshots": [["supercell_c100_t25", "cm1/runs/studio_100m_c/c100_t25_sp.vdb"],
+ ["supercell_s100_t45", "cm1/runs/storm_100m/s100_t45_sp.vdb"],
+ ["supercell_s100_t60", "cm1/runs/storm_100m/s100_t60_sp.vdb"],
+ ["supercell_s250_t45", "cm1/runs/hero_250m/s250_t45_sp.vdb"],
+ ["supercell_s250_t60", "cm1/runs/hero_250m/s250_t60_sp.vdb"]]}
diff --git a/tools/cloud_specs/lasso_sgp_20170403.json b/tools/cloud_specs/lasso_sgp_20170403.json
new file mode 100644
index 0000000..0d6963e
--- /dev/null
+++ b/tools/cloud_specs/lasso_sgp_20170403.json
@@ -0,0 +1,6 @@
+{"set": "lasso_sgp_20170403", "source": "farm_v4_lasso8.blend", "template": "prefix:LASSO_1740_", "normalize": false,
+ "snapshots": [["lasso_1740", "lasso/lasso_1740_lwc.vdb"],
+ ["lasso_1800", "lasso/timelapse_vdb/lwc_0145.vdb"],
+ ["lasso_1900", "lasso/timelapse_vdb/lwc_0193.vdb"],
+ ["lasso_2000", "lasso/timelapse_vdb/lwc_0241.vdb"],
+ ["lasso8_2040", "lasso/lasso8_2040_lwc.vdb"]]}
diff --git a/tools/cloud_specs/ucla_cumulus.json b/tools/cloud_specs/ucla_cumulus.json
new file mode 100644
index 0000000..ad372ef
--- /dev/null
+++ b/tools/cloud_specs/ucla_cumulus.json
@@ -0,0 +1,2 @@
+{"set": "ucla_cumulus", "source": "mirf_s1/mirf_s1_tour_storm.blend", "template": "prefix:UCLA_", "normalize": false,
+ "snapshots": [["ucla_t6600", "lasso/ucla_t6600_m.vdb"]]}
diff --git a/tools/make_catalogue.py b/tools/make_catalogue.py
new file mode 100644
index 0000000..e5cf182
--- /dev/null
+++ b/tools/make_catalogue.py
@@ -0,0 +1,51 @@
+"""Write a README.md catalogue (preview, collection, file) into each asset folder."""
+import json, os, glob
+R = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+TITLES = {
+ 'instruments': ('Instruments', 'One file per instrument; link the `INST_*` (or `CSAPR2`) collection. Root at the origin.'),
+ 'containers': ('Containers', 'AMF2 containers and shelters with their roof-mounted hardware. `CONT_7_SACR_S2` is the SACR container as configured at S2 (KAWG).'),
+ 'vehicles': ('Vehicles', 'Farm vehicles/implements and the ARM Bombardier Challenger 850 (`C850_ARM`, gear/flaps/fans driven from custom properties on `C850_root`).'),
+ 'buildings': ('Buildings and structures', 'Site buildings are stored in site coordinates; the flagpole and center pivots are re-usable (root at the origin).'),
+ 'plants': ('Plants', 'Prototype collections used by the geometry-node scatterers in the site layers. `_HD` sets are the high-detail trees used at S1/S2; `_S2` grasses are the KAWG lawn variants.'),
+ 'clouds': ('Clouds', 'Mature snapshots; one collection per snapshot, VDB next to the .blend. Tiled LES fields (LASSO 3x3, UCLA 7x7) keep their periodic tiling.'),
+}
+
+
+def mb(p):
+ return f'{os.path.getsize(os.path.join(R, p)) / 1e6:.1f} MB'
+
+
+for d, (title, blurb) in TITLES.items():
+ jobs = json.load(open(os.path.join(R, 'tools/preview_jobs', d + '.json')))
+ rows = []
+ for j in jobs:
+ pv = os.path.relpath(os.path.join(R, j['out']), os.path.join(R, d))
+ lib = os.path.relpath(os.path.join(R, j['lib']), os.path.join(R, d))
+ img = f'
' if os.path.exists(os.path.join(R, j['out'])) else '(no preview)'
+ rows.append(f'| {img} | `{j["coll"]}` | [`{lib}`]({lib}) ({mb(j["lib"])}) |')
+ extra = ''
+ if d == 'clouds':
+ extra = '\n\nVDB files:\n\n' + '\n'.join(f'* `{os.path.relpath(v, os.path.join(R, d))}` ({os.path.getsize(v) / 1e6:.1f} MB)'
+ for v in sorted(glob.glob(os.path.join(R, d, '*', '*.vdb'))))
+ with open(os.path.join(R, d, 'README.md'), 'w') as f:
+ f.write(f'# {title}\n\n{blurb}\n\n| Preview | Collection | File |\n|---|---|---|\n' + '\n'.join(rows) + extra + '\n')
+
+# scenes
+rows = []
+for name in ['M1', 'S1', 'S2']:
+ imgs = sorted(glob.glob(os.path.join(R, 'scenes/preview', name + '_*.jpg')))
+ layers = sorted(glob.glob(os.path.join(R, 'scenes', name, 'layers', '*.blend')))
+ rows.append(f'## {name}\n\n`{name}/{name}.blend` ({mb(f"scenes/{name}/{name}.blend")}) — layers: ' +
+ ', '.join(f'`{os.path.relpath(l, os.path.join(R, "scenes"))}` ({os.path.getsize(l) / 1e6:.1f} MB)' for l in layers) + '\n\n' +
+ ' '.join(f'
' for i in imgs))
+tests = sorted(glob.glob(os.path.join(R, 'scenes/tests/*.blend')))
+trow = []
+for t in tests:
+ n = os.path.basename(t)[:-6]
+ i = glob.glob(os.path.join(R, 'scenes/preview', f'test_{n}_*.jpg'))
+ trow.append(f'| `tests/{n}.blend` | ' + (f'
' if i else '') + ' |')
+with open(os.path.join(R, 'scenes/README.md'), 'w') as f:
+ f.write('# Scenes\n\nEach site scene is a thin file holding cameras, lights, world and render settings; '
+ 'everything else is linked from the asset folders and from `layers/` (terrain, fields, vegetation, site infrastructure in site coordinates).\n\n'
+ + '\n\n'.join(rows) + '\n\n## Test scenes\n\n| Scene | Preview |\n|---|---|\n' + '\n'.join(trow) + '\n')
+print('catalogues written')
diff --git a/tools/preview_jobs/buildings.json b/tools/preview_jobs/buildings.json
new file mode 100644
index 0000000..eb2b4e0
--- /dev/null
+++ b/tools/preview_jobs/buildings.json
@@ -0,0 +1,119 @@
+[
+ {
+ "lib": "buildings/m1_farmstead.blend",
+ "coll": "Farmstead",
+ "out": "buildings/preview/Farmstead.jpg",
+ "view": "aerial",
+ "res": [
+ 1280,
+ 800
+ ],
+ "trim": 30,
+ "tight": 0.8,
+ "scene_view": "scenes/preview/M1_Cam_M1_Yard.jpg"
+ },
+ {
+ "lib": "buildings/flagpole.blend",
+ "coll": "Flagpole",
+ "out": "buildings/preview/Flagpole.jpg",
+ "view": "34",
+ "res": [
+ 1280,
+ 800
+ ],
+ "tight": 1.0
+ },
+ {
+ "lib": "buildings/signs/sign_M1.blend",
+ "coll": "SIGN_M1",
+ "out": "buildings/preview/SIGN_M1.jpg",
+ "view": "front_x",
+ "res": [
+ 1280,
+ 800
+ ],
+ "tight": 0.45,
+ "zfrac": 0.82
+ },
+ {
+ "lib": "buildings/signs/sign_S1.blend",
+ "coll": "SIGN_S1",
+ "out": "buildings/preview/SIGN_S1.jpg",
+ "view": "front_x",
+ "res": [
+ 1280,
+ 800
+ ],
+ "tight": 0.45,
+ "zfrac": 0.82
+ },
+ {
+ "lib": "buildings/signs/sign_S2.blend",
+ "coll": "SIGN_S2",
+ "out": "buildings/preview/SIGN_S2.jpg",
+ "view": "front_x",
+ "res": [
+ 1280,
+ 800
+ ],
+ "tight": 0.45,
+ "zfrac": 0.82
+ },
+ {
+ "lib": "buildings/center_pivots.blend",
+ "coll": "Pivot_P3",
+ "out": "buildings/preview/Pivot_P3.jpg",
+ "view": "aerial",
+ "res": [
+ 1280,
+ 800
+ ],
+ "tight": 0.6
+ },
+ {
+ "lib": "buildings/center_pivots.blend",
+ "coll": "Pivot_P1",
+ "out": "buildings/preview/Pivot_P1.jpg",
+ "view": "aerial",
+ "res": [
+ 1280,
+ 800
+ ],
+ "tight": 0.6
+ },
+ {
+ "lib": "buildings/louisa_station.blend",
+ "coll": "LOUISA_Station",
+ "out": "buildings/preview/LOUISA_Station.jpg",
+ "view": "aerial",
+ "res": [
+ 1280,
+ 800
+ ]
+ },
+ {
+ "lib": "buildings/s1_mirf_buildings.blend",
+ "coll": "S1_MIRF_Buildings",
+ "out": "buildings/preview/S1_MIRF_Buildings.jpg",
+ "view": "aerial",
+ "res": [
+ 1280,
+ 800
+ ],
+ "trim": 12,
+ "tight": 0.75,
+ "scene_view": "scenes/preview/S1_Cam_Aerial_SW.jpg"
+ },
+ {
+ "lib": "buildings/s2_kawg_buildings.blend",
+ "coll": "S2_KAWG_Buildings",
+ "out": "buildings/preview/S2_KAWG_Buildings.jpg",
+ "view": "aerial",
+ "res": [
+ 1280,
+ 800
+ ],
+ "trim": 5,
+ "tight": 0.65
+ }
+]
\ No newline at end of file
diff --git a/tools/preview_jobs/clouds.json b/tools/preview_jobs/clouds.json
new file mode 100644
index 0000000..9f10d5b
--- /dev/null
+++ b/tools/preview_jobs/clouds.json
@@ -0,0 +1,172 @@
+[
+ {
+ "lib": "clouds/cm1_bomex_cumulus/cm1_bomex_cumulus.blend",
+ "coll": "CLOUD_bomex_t360",
+ "out": "clouds/preview/bomex_t360.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24
+ },
+ {
+ "lib": "clouds/cm1_bomex_cumulus/cm1_bomex_cumulus.blend",
+ "coll": "CLOUD_bomex_t300",
+ "out": "clouds/preview/bomex_t300.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24
+ },
+ {
+ "lib": "clouds/cm1_bomex_cumulus/cm1_bomex_cumulus.blend",
+ "coll": "CLOUD_bomex_t240",
+ "out": "clouds/preview/bomex_t240.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24
+ },
+ {
+ "lib": "clouds/cm1_supercell/cm1_supercell.blend",
+ "coll": "CLOUD_supercell_s250_t60",
+ "out": "clouds/preview/supercell_s250_t60.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 16,
+ "volume_bounces": 48,
+ "dist": 0.55,
+ "cam_z": 1500.0
+ },
+ {
+ "lib": "clouds/cm1_supercell/cm1_supercell.blend",
+ "coll": "CLOUD_supercell_s250_t45",
+ "out": "clouds/preview/supercell_s250_t45.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 16,
+ "volume_bounces": 48,
+ "dist": 0.55,
+ "cam_z": 1500.0
+ },
+ {
+ "lib": "clouds/cm1_supercell/cm1_supercell.blend",
+ "coll": "CLOUD_supercell_s100_t60",
+ "out": "clouds/preview/supercell_s100_t60.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 16,
+ "volume_bounces": 48,
+ "dist": 0.55,
+ "cam_z": 1500.0
+ },
+ {
+ "lib": "clouds/cm1_supercell/cm1_supercell.blend",
+ "coll": "CLOUD_supercell_s100_t45",
+ "out": "clouds/preview/supercell_s100_t45.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 16,
+ "volume_bounces": 48,
+ "dist": 0.55,
+ "cam_z": 1500.0
+ },
+ {
+ "lib": "clouds/cm1_supercell/cm1_supercell.blend",
+ "coll": "CLOUD_supercell_c100_t25",
+ "out": "clouds/preview/supercell_c100_t25.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 16,
+ "volume_bounces": 48,
+ "scene_view": "scenes/preview/test_m1_supercell_Cam_Storm_Sign.jpg"
+ },
+ {
+ "lib": "clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend",
+ "coll": "CLOUD_lasso_2000",
+ "out": "clouds/preview/lasso_2000.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24
+ },
+ {
+ "lib": "clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend",
+ "coll": "CLOUD_lasso_1900",
+ "out": "clouds/preview/lasso_1900.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24
+ },
+ {
+ "lib": "clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend",
+ "coll": "CLOUD_lasso_1800",
+ "out": "clouds/preview/lasso_1800.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24
+ },
+ {
+ "lib": "clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend",
+ "coll": "CLOUD_lasso_1740",
+ "out": "clouds/preview/lasso_1740.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24
+ },
+ {
+ "lib": "clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend",
+ "coll": "CLOUD_lasso8_2040",
+ "out": "clouds/preview/lasso8_2040.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24,
+ "scene_view": "scenes/preview/test_m1_lasso_cumulus_Cam_M1_Aerial_SE.jpg"
+ },
+ {
+ "lib": "clouds/ucla_cumulus/ucla_cumulus.blend",
+ "coll": "CLOUD_ucla_t6600",
+ "out": "clouds/preview/ucla_t6600.jpg",
+ "view": "cloud",
+ "res": [
+ 960,
+ 540
+ ],
+ "samples": 24,
+ "scene_view": "scenes/preview/test_s1_ucla_cumulus_Cam_Aerial_SW.jpg"
+ }
+]
\ No newline at end of file
diff --git a/tools/preview_jobs/containers.json b/tools/preview_jobs/containers.json
new file mode 100644
index 0000000..78961c3
--- /dev/null
+++ b/tools/preview_jobs/containers.json
@@ -0,0 +1,134 @@
+[
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_RentalContainer",
+ "out": "containers/preview/CONT_RentalContainer.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_OfficeShelter",
+ "out": "containers/preview/CONT_OfficeShelter.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_AOS_Platform",
+ "out": "containers/preview/CONT_AOS_Platform.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_8_STORAGE",
+ "out": "containers/preview/CONT_8_STORAGE.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_7_SACR",
+ "out": "containers/preview/CONT_7_SACR.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_6_KAZR",
+ "out": "containers/preview/CONT_6_KAZR.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_5_LIDAR",
+ "out": "containers/preview/CONT_5_LIDAR.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_4_RADIOMETERS",
+ "out": "containers/preview/CONT_4_RADIOMETERS.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_3_OPS",
+ "out": "containers/preview/CONT_3_OPS.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_2_AOS_B",
+ "out": "containers/preview/CONT_2_AOS_B.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/amf2_containers.blend",
+ "coll": "CONT_1_AOS_A",
+ "out": "containers/preview/CONT_1_AOS_A.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ },
+ {
+ "lib": "containers/sacr_container_s2.blend",
+ "coll": "CONT_7_SACR_S2",
+ "out": "containers/preview/CONT_7_SACR_S2.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.95
+ }
+]
\ No newline at end of file
diff --git a/tools/preview_jobs/instruments.json b/tools/preview_jobs/instruments.json
new file mode 100644
index 0000000..92587d2
--- /dev/null
+++ b/tools/preview_jobs/instruments.json
@@ -0,0 +1,277 @@
+[
+ {
+ "lib": "instruments/AERI/AERI.blend",
+ "coll": "INST_AERI",
+ "out": "instruments/preview/AERI.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/AOSMET/AOSMET.blend",
+ "coll": "INST_AOSMET",
+ "out": "instruments/preview/AOSMET.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/ASI/ASI.blend",
+ "coll": "INST_ASI",
+ "out": "instruments/preview/ASI.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/AWOS/AWOS.blend",
+ "coll": "INST_AWOS",
+ "out": "instruments/preview/AWOS.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/CEIL/CEIL.blend",
+ "coll": "INST_CEIL",
+ "out": "instruments/preview/CEIL.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/CSAPR2/CSAPR2.blend",
+ "coll": "CSAPR2",
+ "out": "instruments/preview/CSAPR2.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/CSPHOT/CSPHOT.blend",
+ "coll": "INST_CSPHOT",
+ "out": "instruments/preview/CSPHOT.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/DL/DL.blend",
+ "coll": "INST_DL",
+ "out": "instruments/preview/DL.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/ECOR/ECOR.blend",
+ "coll": "INST_ECOR_4m",
+ "out": "instruments/preview/ECOR_4m.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/ECOR/ECOR.blend",
+ "coll": "INST_ECOR",
+ "out": "instruments/preview/ECOR.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/IRT/IRT.blend",
+ "coll": "INST_IRT",
+ "out": "instruments/preview/IRT.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/KAZR/KAZR.blend",
+ "coll": "INST_KAZR",
+ "out": "instruments/preview/KAZR.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/LDIS/LDIS.blend",
+ "coll": "INST_LDIS",
+ "out": "instruments/preview/LDIS.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/MAWS/MAWS.blend",
+ "coll": "INST_MAWS",
+ "out": "instruments/preview/MAWS.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/MET/MET.blend",
+ "coll": "INST_MET",
+ "out": "instruments/preview/MET.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/MFR/MFR.blend",
+ "coll": "INST_MFR",
+ "out": "instruments/preview/MFR.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/MFRSR/MFRSR.blend",
+ "coll": "INST_MFRSR",
+ "out": "instruments/preview/MFRSR.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/MPL/MPL.blend",
+ "coll": "INST_MPL",
+ "out": "instruments/preview/MPL.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/MWR3C/MWR3C.blend",
+ "coll": "INST_MWR3C",
+ "out": "instruments/preview/MWR3C.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/RWP/RWP.blend",
+ "coll": "INST_RWP",
+ "out": "instruments/preview/RWP.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/SEBS/SEBS.blend",
+ "coll": "INST_SEBS",
+ "out": "instruments/preview/SEBS.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/SIRS/SIRS.blend",
+ "coll": "INST_SIRS",
+ "out": "instruments/preview/SIRS.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/SONDE/SONDE.blend",
+ "coll": "INST_SONDE",
+ "out": "instruments/preview/SONDE.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/VDIS/VDIS.blend",
+ "coll": "INST_VDIS",
+ "out": "instruments/preview/VDIS.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ },
+ {
+ "lib": "instruments/WB/WB.blend",
+ "coll": "INST_WB",
+ "out": "instruments/preview/WB.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "samples": 64
+ }
+]
\ No newline at end of file
diff --git a/tools/preview_jobs/plants.json b/tools/preview_jobs/plants.json
new file mode 100644
index 0000000..65366be
--- /dev/null
+++ b/tools/preview_jobs/plants.json
@@ -0,0 +1,122 @@
+[
+ {
+ "lib": "plants/crops.blend",
+ "coll": "CornVariants",
+ "out": "plants/preview/CornVariants.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/crops.blend",
+ "coll": "SoyVariants",
+ "out": "plants/preview/SoyVariants.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/grasses.blend",
+ "coll": "GrassVariants",
+ "out": "plants/preview/GrassVariants.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/grasses.blend",
+ "coll": "TallGrassVariants",
+ "out": "plants/preview/TallGrassVariants.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/grasses_s2.blend",
+ "coll": "GrassVariants_S2",
+ "out": "plants/preview/GrassVariants_S2.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/grasses_s2.blend",
+ "coll": "TallGrassVariants_S2",
+ "out": "plants/preview/TallGrassVariants_S2.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/trees.blend",
+ "coll": "TreeVariants",
+ "out": "plants/preview/TreeVariants.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/trees_hd.blend",
+ "coll": "TV_Decid_HD",
+ "out": "plants/preview/TV_Decid_HD.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/trees_hd.blend",
+ "coll": "TV_Spruce_HD",
+ "out": "plants/preview/TV_Spruce_HD.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/trees_hd.blend",
+ "coll": "TV_DecidBaked_HD",
+ "out": "plants/preview/TV_DecidBaked_HD.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/trees_hd.blend",
+ "coll": "TV_Far_HD",
+ "out": "plants/preview/TV_Far_HD.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ },
+ {
+ "lib": "plants/native_flowers.blend",
+ "coll": "NativeFlowerVariants",
+ "out": "plants/preview/NativeFlowerVariants.jpg",
+ "view": "lineup",
+ "res": [
+ 1600,
+ 700
+ ]
+ }
+]
\ No newline at end of file
diff --git a/tools/preview_jobs/vehicles.json b/tools/preview_jobs/vehicles.json
new file mode 100644
index 0000000..7f6b7ce
--- /dev/null
+++ b/tools/preview_jobs/vehicles.json
@@ -0,0 +1,90 @@
+[
+ {
+ "lib": "vehicles/farm_vehicles.blend",
+ "coll": "VEH_Tractor_Green",
+ "out": "vehicles/preview/VEH_Tractor_Green.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.9
+ },
+ {
+ "lib": "vehicles/farm_vehicles.blend",
+ "coll": "VEH_Telehandler_Yellow",
+ "out": "vehicles/preview/VEH_Telehandler_Yellow.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.9
+ },
+ {
+ "lib": "vehicles/farm_vehicles.blend",
+ "coll": "VEH_Pickup_White",
+ "out": "vehicles/preview/VEH_Pickup_White.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.9
+ },
+ {
+ "lib": "vehicles/farm_vehicles.blend",
+ "coll": "VEH_Implement_Disk_Green",
+ "out": "vehicles/preview/VEH_Implement_Disk_Green.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.9
+ },
+ {
+ "lib": "vehicles/farm_vehicles.blend",
+ "coll": "VEH_GrainCart_Red",
+ "out": "vehicles/preview/VEH_GrainCart_Red.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.9
+ },
+ {
+ "lib": "vehicles/farm_vehicles.blend",
+ "coll": "VEH_Gooseneck_Flatbed",
+ "out": "vehicles/preview/VEH_Gooseneck_Flatbed.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.9
+ },
+ {
+ "lib": "vehicles/farm_vehicles.blend",
+ "coll": "VEH_Cultivator_Red",
+ "out": "vehicles/preview/VEH_Cultivator_Red.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.9
+ },
+ {
+ "lib": "vehicles/c850_arm.blend",
+ "coll": "C850_ARM",
+ "out": "vehicles/preview/C850_ARM.jpg",
+ "view": "34",
+ "res": [
+ 1024,
+ 768
+ ],
+ "tight": 0.62
+ }
+]
\ No newline at end of file
diff --git a/tools/render_previews.py b/tools/render_previews.py
new file mode 100644
index 0000000..6589e20
--- /dev/null
+++ b/tools/render_previews.py
@@ -0,0 +1,166 @@
+"""Studio preview renders of library assets.
+blender -b --factory-startup --python tools/render_previews.py -- jobs.json
+Each job: {"lib": "instruments/AERI/AERI.blend", "coll": "INST_AERI", "out": "instruments/preview/AERI.jpg",
+ "view": "34" | "aerial" | "lineup" | "cloud", "samples": 64, "res": [1280, 960]}
+The world (sky) and sun are linked from scenes/M1/M1.blend so previews match the scenes.
+"""
+import bpy, sys, os, json, math
+from mathutils import Vector, Matrix
+REPO = os.environ.get('ABLE_REPO') or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+jobs = json.load(open(sys.argv[sys.argv.index('--') + 1]))
+
+# ---- one-time studio setup
+sc = bpy.context.scene
+for o in list(bpy.data.objects):
+ bpy.data.objects.remove(o)
+m1 = os.path.join(REPO, 'scenes/M1/M1.blend')
+with bpy.data.libraries.load(m1, link=True, relative=False) as (s, d):
+ d.worlds = ['World']
+ d.objects = ['Sun']
+sc.world = d.worlds[0]
+sun = d.objects[0]
+with bpy.data.libraries.load(m1, link=True, relative=False) as (s2, d2):
+ d2.scenes = [s2.scenes[0]]
+src = d2.scenes[0]
+for attr in ('view_transform', 'look', 'exposure', 'gamma'):
+ try:
+ setattr(sc.view_settings, attr, getattr(src.view_settings, attr))
+ except Exception as e:
+ print('view setting', attr, e)
+sc.display_settings.display_device = src.display_settings.display_device
+sc.collection.objects.link(sun)
+ground_mat = bpy.data.materials.new('PreviewGround')
+ground_mat.use_nodes = True
+bsdf = ground_mat.node_tree.nodes.get('Principled BSDF')
+bsdf.inputs['Base Color'].default_value = (0.32, 0.30, 0.27, 1)
+bsdf.inputs['Roughness'].default_value = 0.9
+r = sc.render
+r.engine = 'CYCLES'
+sc.cycles.use_denoising = True
+r.image_settings.file_format = 'JPEG'; r.image_settings.quality = 90
+try:
+ p = bpy.context.preferences.addons['cycles'].preferences
+ p.compute_device_type = 'METAL'; p.get_devices()
+ for dv in p.devices: dv.use = True
+ sc.cycles.device = 'GPU'
+except Exception as e:
+ print('GPU setup failed', e)
+
+cam_data = bpy.data.cameras.new('PreviewCam'); cam_data.lens = 50
+cam = bpy.data.objects.new('PreviewCam', cam_data); sc.collection.objects.link(cam); sc.camera = cam
+
+
+def link_coll(lib, name):
+ with bpy.data.libraries.load(os.path.join(REPO, lib), link=True, relative=False) as (s, d):
+ d.collections = [name]
+ return d.collections[0]
+
+
+def bounds(empties, trim=0.0):
+ dg = bpy.context.evaluated_depsgraph_get()
+ lo = Vector((1e18,) * 3); hi = Vector((-1e18,) * 3); n = 0
+ pts = []
+ for inst in dg.object_instances:
+ if not inst.is_instance or inst.parent is None or inst.parent.original not in empties:
+ continue
+ o = inst.object
+ if o.type in ('CAMERA', 'LIGHT', 'EMPTY'):
+ continue
+ for c in o.bound_box:
+ w = inst.matrix_world @ Vector(c)
+ pts.append(w)
+ lo = Vector(map(min, lo, w)); hi = Vector(map(max, hi, w)); n += 1
+ if trim and pts:
+ import numpy as np
+ a = np.array([tuple(p) for p in pts])
+ lo = Vector(np.percentile(a, trim, axis=0)); hi = Vector(np.percentile(a, 100 - trim, axis=0))
+ return lo, hi, n
+
+
+def frame(lo, hi, view, res, j_tight=0.72, zfrac=0.5):
+ ctr = (lo + hi) / 2; ext = hi - lo
+ ctr.z = lo.z + zfrac * ext.z
+ rad = max(ext.length / 2, 0.3)
+ d = {'34': Vector((-1.0, -1.35, 0.62)), 'aerial': Vector((-0.75, -1.0, 1.15)),
+ 'lineup': Vector((0.0, -1.0, 0.28)), 'cloud': Vector((-0.6, -1.0, 0.22)),
+ 'top': Vector((0.0, -0.02, 1.0)), 'front_x': Vector((1.0, -0.35, 0.22))}[view].normalized()
+ half = math.atan(18.0 / cam_data.lens) * (min(res) / max(res) if res[0] >= res[1] else 1)
+ dist = rad / math.sin(half) * j_tight
+ cam.location = ctr + d * dist
+ cam.rotation_euler = (-d).to_track_quat('-Z', 'Y').to_euler()
+ cam_data.clip_start = max(0.01, dist / 2000); cam_data.clip_end = dist * 4 + ext.length * 2
+ return ctr, ext
+
+
+made = []
+for j in jobs:
+ if j.get('scene_view'): # site layers spread over kilometres read better in their scene
+ import shutil
+ shutil.copy(os.path.join(REPO, j['scene_view']), os.path.join(REPO, j['out']))
+ continue
+ res = j.get('res', [1280, 960]); r.resolution_x, r.resolution_y = res; r.resolution_percentage = 100
+ sc.cycles.samples = j.get('samples', 64)
+ # cloud scenes in the working files use deep volume scattering
+ sc.cycles.volume_bounces = j.get('volume_bounces', 16) if j.get('view') == 'cloud' else 1
+ sc.cycles.max_bounces = 20 if j.get('view') == 'cloud' else 4
+ sc.cycles.sample_clamp_indirect = 10.0
+ view = j.get('view', '34')
+ coll = link_coll(j['lib'], j['coll'])
+ for ob in coll.all_objects: # volume bounds are only known once the grids are read
+ if ob.type == 'VOLUME':
+ ob.data.grids.load()
+ empties = []
+ if view == 'lineup':
+ objs = sorted([o for o in coll.all_objects if o.type in ('MESH', 'CURVE', 'VOLUME') and o.parent is None], key=lambda o: o.name)
+ x = 0.0
+ for o in objs:
+ dims = Vector(o.dimensions) if any(o.dimensions) else Vector((1, 1, 1))
+ if dims.length == 0: dims = Vector((1, 1, 1))
+ c = bpy.data.collections.new('LU_' + o.name); c.objects.link(o)
+ c.instance_offset = o.matrix_world.translation
+ e = bpy.data.objects.new('LU_' + o.name, None); e.instance_type = 'COLLECTION'; e.instance_collection = c
+ w = max(dims.x, dims.y, 0.2)
+ e.location = (x + w / 2, 0, 0); x += w * 1.25
+ sc.collection.objects.link(e); empties.append(e)
+ else:
+ e = bpy.data.objects.new('PV_' + j['coll'], None); e.instance_type = 'COLLECTION'; e.instance_collection = coll
+ sc.collection.objects.link(e); empties.append(e)
+ bpy.context.view_layer.update()
+ lo, hi, n = bounds(empties, j.get('trim', 0.0))
+ if n == 0:
+ print('NOTHING TO FRAME', j); continue
+ if view == 'cloud':
+ ctr = (lo + hi) / 2; ext = hi - lo
+ L = sun.matrix_world.to_3x3() @ Vector((0, 0, -1))
+ h = Vector((L.x, L.y, 0)).normalized()
+ wide = max(ext.x, ext.y)
+ nvol = sum(1 for ob in coll.all_objects if ob.type == 'VOLUME')
+ if nvol > 1: # tiled periodic LES field: stand inside it
+ cam_data.lens = 20
+ cam.location = Vector((ctr.x, ctr.y, 2.0)) - h * (0.18 * wide)
+ target = cam.location + h * 10000 + Vector((0, 0, 2600))
+ else: # single storm: elevated, sun behind, whole cell in view
+ cam_data.lens = 35
+ cam.location = Vector((ctr.x, ctr.y, j.get('cam_z', 3000.0))) - h * (j.get('dist', 2.2) * wide)
+ target = Vector((ctr.x, ctr.y, lo.z + 0.3 * ext.z))
+ cam.rotation_euler = (target - cam.location).to_track_quat('-Z', 'Y').to_euler()
+ cam_data.clip_start = 0.5; cam_data.clip_end = wide * 6
+ ctr = Vector((cam.location.x, cam.location.y, 0)); ext = Vector((wide, wide, ext.z))
+ else:
+ cam_data.lens = 50
+ ctr, ext = frame(lo, hi, view, res, j.get('tight', 0.72 if view in ('34', 'lineup') else 0.85), j.get('zfrac', 0.5))
+ gsize = max(ext.x, ext.y, 1.0) * (40 if view != 'cloud' else 12)
+ bpy.ops.mesh.primitive_plane_add(size=gsize, location=(ctr.x, ctr.y, lo.z - 0.002 if view != 'cloud' else 0.0))
+ g = bpy.context.active_object; g.data.materials.append(ground_mat)
+ out = os.path.join(REPO, j['out']); os.makedirs(os.path.dirname(out), exist_ok=True)
+ r.filepath = out
+ bpy.ops.render.render(write_still=True)
+ made.append(j['out']); print('PREVIEW', j['out'], flush=True)
+ # clean up for the next job
+ bpy.data.objects.remove(g)
+ for e in empties:
+ ic = e.instance_collection
+ bpy.data.objects.remove(e)
+ if ic.name.startswith('LU_'):
+ bpy.data.collections.remove(ic)
+print('PREVIEWS_DONE', len(made))
diff --git a/tools/render_scene_views.py b/tools/render_scene_views.py
new file mode 100644
index 0000000..728fcd1
--- /dev/null
+++ b/tools/render_scene_views.py
@@ -0,0 +1,26 @@
+"""Render preview stills of a scene from its own cameras (scene lighting and settings kept).
+blender -b scenes/M1/M1.blend --python tools/render_scene_views.py -- [samples] [WxH]
+"""
+import bpy, sys, os
+args = sys.argv[sys.argv.index('--') + 1:]
+prefix, cams = args[0], args[1].split(',')
+spp = int(args[2]) if len(args) > 2 else 96
+sc = bpy.context.scene; r = sc.render
+r.engine = 'CYCLES'; sc.cycles.samples = spp; sc.cycles.use_denoising = True
+w, h = (int(v) for v in args[3].split('x')) if len(args) > 3 else (1600, 900)
+r.resolution_x, r.resolution_y, r.resolution_percentage = w, h, 100
+if len(args) > 4: # preview-only cap on volume bounces (the scene file keeps its own setting)
+ sc.cycles.volume_bounces = min(sc.cycles.volume_bounces, int(args[4]))
+r.image_settings.file_format = 'JPEG'; r.image_settings.quality = 90
+try:
+ p = bpy.context.preferences.addons['cycles'].preferences
+ p.compute_device_type = 'METAL'; p.get_devices()
+ for d in p.devices: d.use = True
+ sc.cycles.device = 'GPU'
+except Exception as e:
+ print('GPU setup failed', e)
+for c in cams:
+ sc.camera = bpy.data.objects[c]
+ r.filepath = f'{prefix}_{c}.jpg'
+ bpy.ops.render.render(write_still=True)
+ print('VIEW', r.filepath, flush=True)
diff --git a/tools/reports/build_m1.json b/tools/reports/build_m1.json
new file mode 100644
index 0000000..29d3170
--- /dev/null
+++ b/tools/reports/build_m1.json
@@ -0,0 +1,488 @@
+{
+ "written": [
+ [
+ "plants/crops.blend",
+ [
+ "CornVariants",
+ "SoyVariants"
+ ],
+ 655756
+ ],
+ [
+ "plants/grasses.blend",
+ [
+ "GrassVariants",
+ "TallGrassVariants"
+ ],
+ 282645
+ ],
+ [
+ "plants/trees.blend",
+ [
+ "TreeVariants",
+ "TV_Decid",
+ "TV_Spruce",
+ "TV_Far",
+ "TreeParts"
+ ],
+ 3353180
+ ],
+ [
+ "scenes/M1/layers/m1_ground.blend",
+ [
+ "Ground"
+ ],
+ 45358694
+ ],
+ [
+ "instruments/AERI/AERI.blend",
+ [
+ "INST_AERI"
+ ],
+ 172354
+ ],
+ [
+ "instruments/AOSMET/AOSMET.blend",
+ [
+ "INST_AOSMET"
+ ],
+ 135496
+ ],
+ [
+ "instruments/KAZR/KAZR.blend",
+ [
+ "INST_KAZR"
+ ],
+ 211996
+ ],
+ [
+ "instruments/DL/DL.blend",
+ [
+ "INST_DL"
+ ],
+ 181669
+ ],
+ [
+ "instruments/MPL/MPL.blend",
+ [
+ "INST_MPL"
+ ],
+ 143107
+ ],
+ [
+ "instruments/MWR3C/MWR3C.blend",
+ [
+ "INST_MWR3C"
+ ],
+ 150404
+ ],
+ [
+ "instruments/MET/MET.blend",
+ [
+ "INST_MET"
+ ],
+ 260782
+ ],
+ [
+ "instruments/ECOR/ECOR.blend",
+ [
+ "INST_ECOR",
+ "INST_ECOR_4m"
+ ],
+ 261872
+ ],
+ [
+ "instruments/SEBS/SEBS.blend",
+ [
+ "INST_SEBS"
+ ],
+ 187116
+ ],
+ [
+ "instruments/WB/WB.blend",
+ [
+ "INST_WB"
+ ],
+ 191477
+ ],
+ [
+ "instruments/LDIS/LDIS.blend",
+ [
+ "INST_LDIS"
+ ],
+ 138280
+ ],
+ [
+ "instruments/VDIS/VDIS.blend",
+ [
+ "INST_VDIS"
+ ],
+ 130349
+ ],
+ [
+ "instruments/MFRSR/MFRSR.blend",
+ [
+ "INST_MFRSR"
+ ],
+ 131271
+ ],
+ [
+ "instruments/SIRS/SIRS.blend",
+ [
+ "INST_SIRS"
+ ],
+ 283836
+ ],
+ [
+ "instruments/CSPHOT/CSPHOT.blend",
+ [
+ "INST_CSPHOT"
+ ],
+ 152991
+ ],
+ [
+ "instruments/ASI/ASI.blend",
+ [
+ "INST_ASI"
+ ],
+ 191681
+ ],
+ [
+ "instruments/CEIL/CEIL.blend",
+ [
+ "INST_CEIL"
+ ],
+ 120682
+ ],
+ [
+ "instruments/RWP/RWP.blend",
+ [
+ "INST_RWP"
+ ],
+ 282435
+ ],
+ [
+ "instruments/IRT/IRT.blend",
+ [
+ "INST_IRT"
+ ],
+ 139775
+ ],
+ [
+ "instruments/MAWS/MAWS.blend",
+ [
+ "INST_MAWS"
+ ],
+ 164670
+ ],
+ [
+ "instruments/MFR/MFR.blend",
+ [
+ "INST_MFR"
+ ],
+ 117949
+ ],
+ [
+ "instruments/SONDE/SONDE.blend",
+ [
+ "INST_SONDE"
+ ],
+ 213176
+ ],
+ [
+ "containers/amf2_containers.blend",
+ [
+ "CONT_1_AOS_A",
+ "CONT_2_AOS_B",
+ "CONT_3_OPS",
+ "CONT_4_RADIOMETERS",
+ "CONT_5_LIDAR",
+ "CONT_6_KAZR",
+ "CONT_7_SACR",
+ "CONT_8_STORAGE",
+ "CONT_AOS_Platform",
+ "CONT_OfficeShelter",
+ "CONT_RentalContainer"
+ ],
+ 242057
+ ],
+ [
+ "vehicles/farm_vehicles.blend",
+ [
+ "VEH_Tractor_Green",
+ "VEH_Telehandler_Yellow",
+ "VEH_Pickup_White",
+ "VEH_Implement_Disk_Green",
+ "VEH_GrainCart_Red",
+ "VEH_Gooseneck_Flatbed",
+ "VEH_Cultivator_Red"
+ ],
+ 1034626
+ ],
+ [
+ "buildings/m1_farmstead.blend",
+ [
+ "Farmstead"
+ ],
+ 270938
+ ],
+ [
+ "buildings/flagpole.blend",
+ [
+ "Flagpole"
+ ],
+ 196185
+ ],
+ [
+ "buildings/signs/sign_M1.blend",
+ [
+ "SIGN_M1"
+ ],
+ 876480
+ ],
+ [
+ "scenes/M1/layers/m1_fields.blend",
+ [
+ "Fields"
+ ],
+ 11841152
+ ],
+ [
+ "scenes/M1/layers/m1_amf2_site.blend",
+ [
+ "AMF2"
+ ],
+ 228961
+ ]
+ ],
+ "linked": [
+ [
+ "plants/crops.blend",
+ [
+ "CornVariants",
+ "SoyVariants"
+ ]
+ ],
+ [
+ "plants/grasses.blend",
+ [
+ "GrassVariants",
+ "TallGrassVariants"
+ ]
+ ],
+ [
+ "plants/trees.blend",
+ [
+ "TreeVariants",
+ "TV_Decid",
+ "TV_Spruce",
+ "TV_Far",
+ "TreeParts"
+ ]
+ ],
+ [
+ "scenes/M1/layers/m1_ground.blend",
+ [
+ "Ground"
+ ]
+ ],
+ [
+ "instruments/AERI/AERI.blend",
+ [
+ "INST_AERI"
+ ]
+ ],
+ [
+ "instruments/AOSMET/AOSMET.blend",
+ [
+ "INST_AOSMET"
+ ]
+ ],
+ [
+ "instruments/KAZR/KAZR.blend",
+ [
+ "INST_KAZR"
+ ]
+ ],
+ [
+ "instruments/DL/DL.blend",
+ [
+ "INST_DL"
+ ]
+ ],
+ [
+ "instruments/MPL/MPL.blend",
+ [
+ "INST_MPL"
+ ]
+ ],
+ [
+ "instruments/MWR3C/MWR3C.blend",
+ [
+ "INST_MWR3C"
+ ]
+ ],
+ [
+ "instruments/MET/MET.blend",
+ [
+ "INST_MET"
+ ]
+ ],
+ [
+ "instruments/ECOR/ECOR.blend",
+ [
+ "INST_ECOR",
+ "INST_ECOR_4m"
+ ]
+ ],
+ [
+ "instruments/SEBS/SEBS.blend",
+ [
+ "INST_SEBS"
+ ]
+ ],
+ [
+ "instruments/WB/WB.blend",
+ [
+ "INST_WB"
+ ]
+ ],
+ [
+ "instruments/LDIS/LDIS.blend",
+ [
+ "INST_LDIS"
+ ]
+ ],
+ [
+ "instruments/VDIS/VDIS.blend",
+ [
+ "INST_VDIS"
+ ]
+ ],
+ [
+ "instruments/MFRSR/MFRSR.blend",
+ [
+ "INST_MFRSR"
+ ]
+ ],
+ [
+ "instruments/SIRS/SIRS.blend",
+ [
+ "INST_SIRS"
+ ]
+ ],
+ [
+ "instruments/CSPHOT/CSPHOT.blend",
+ [
+ "INST_CSPHOT"
+ ]
+ ],
+ [
+ "instruments/ASI/ASI.blend",
+ [
+ "INST_ASI"
+ ]
+ ],
+ [
+ "instruments/CEIL/CEIL.blend",
+ [
+ "INST_CEIL"
+ ]
+ ],
+ [
+ "instruments/RWP/RWP.blend",
+ [
+ "INST_RWP"
+ ]
+ ],
+ [
+ "instruments/IRT/IRT.blend",
+ [
+ "INST_IRT"
+ ]
+ ],
+ [
+ "instruments/MAWS/MAWS.blend",
+ [
+ "INST_MAWS"
+ ]
+ ],
+ [
+ "instruments/MFR/MFR.blend",
+ [
+ "INST_MFR"
+ ]
+ ],
+ [
+ "instruments/SONDE/SONDE.blend",
+ [
+ "INST_SONDE"
+ ]
+ ],
+ [
+ "containers/amf2_containers.blend",
+ [
+ "CONT_1_AOS_A",
+ "CONT_2_AOS_B",
+ "CONT_3_OPS",
+ "CONT_4_RADIOMETERS",
+ "CONT_5_LIDAR",
+ "CONT_6_KAZR",
+ "CONT_7_SACR",
+ "CONT_8_STORAGE",
+ "CONT_AOS_Platform",
+ "CONT_OfficeShelter",
+ "CONT_RentalContainer"
+ ]
+ ],
+ [
+ "vehicles/farm_vehicles.blend",
+ [
+ "VEH_Tractor_Green",
+ "VEH_Telehandler_Yellow",
+ "VEH_Pickup_White",
+ "VEH_Implement_Disk_Green",
+ "VEH_GrainCart_Red",
+ "VEH_Gooseneck_Flatbed",
+ "VEH_Cultivator_Red"
+ ]
+ ],
+ [
+ "buildings/m1_farmstead.blend",
+ [
+ "Farmstead"
+ ]
+ ],
+ [
+ "buildings/flagpole.blend",
+ [
+ "Flagpole"
+ ]
+ ],
+ [
+ "buildings/signs/sign_M1.blend",
+ [
+ "SIGN_M1"
+ ]
+ ],
+ [
+ "scenes/M1/layers/m1_fields.blend",
+ [
+ "Fields"
+ ]
+ ],
+ [
+ "scenes/M1/layers/m1_amf2_site.blend",
+ [
+ "AMF2"
+ ]
+ ]
+ ],
+ "missing": [],
+ "warnings": [],
+ "mismatch": [],
+ "scene": [
+ "scenes/M1/M1.blend",
+ 2318821
+ ]
+}
\ No newline at end of file
diff --git a/tools/reports/build_s1.json b/tools/reports/build_s1.json
new file mode 100644
index 0000000..ec42075
--- /dev/null
+++ b/tools/reports/build_s1.json
@@ -0,0 +1,150 @@
+{
+ "written": [
+ [
+ "plants/trees_hd.blend",
+ [
+ "TV_Spruce_HD",
+ "TV_Decid_HD",
+ "TV_Far_HD",
+ "TreeParts_HD",
+ "TV_DecidBaked_HD"
+ ],
+ 42908567
+ ],
+ [
+ "plants/native_flowers.blend",
+ [
+ "NativeFlowerVariants"
+ ],
+ 165634
+ ],
+ [
+ "scenes/S1/layers/s1_terrain.blend",
+ [
+ "Terrain"
+ ],
+ 41189975
+ ],
+ [
+ "buildings/center_pivots.blend",
+ [
+ "Pivot_P1",
+ "Pivot_P2",
+ "Pivot_P3"
+ ],
+ 1488550
+ ],
+ [
+ "buildings/louisa_station.blend",
+ [
+ "LOUISA_Station",
+ "LOUISA_TreePoints"
+ ],
+ 405090
+ ],
+ [
+ "buildings/s1_mirf_buildings.blend",
+ [
+ "S1_MIRF_Buildings"
+ ],
+ 107641
+ ],
+ [
+ "buildings/signs/sign_S1.blend",
+ [
+ "SIGN_S1"
+ ],
+ 868999
+ ],
+ [
+ "scenes/S1/layers/s1_vegetation.blend",
+ [
+ "Vegetation"
+ ],
+ 8429495
+ ]
+ ],
+ "linked": [
+ [
+ "plants/crops.blend",
+ [
+ "CornVariants",
+ "SoyVariants"
+ ]
+ ],
+ [
+ "plants/trees_hd.blend",
+ [
+ "TV_Spruce_HD",
+ "TV_Decid_HD",
+ "TV_Far_HD",
+ "TreeParts_HD",
+ "TV_DecidBaked_HD"
+ ]
+ ],
+ [
+ "plants/native_flowers.blend",
+ [
+ "NativeFlowerVariants"
+ ]
+ ],
+ [
+ "scenes/S1/layers/s1_terrain.blend",
+ [
+ "Terrain"
+ ]
+ ],
+ [
+ "instruments/ECOR/ECOR.blend",
+ [
+ "INST_ECOR_4m"
+ ]
+ ],
+ [
+ "instruments/SEBS/SEBS.blend",
+ [
+ "INST_SEBS"
+ ]
+ ],
+ [
+ "buildings/center_pivots.blend",
+ [
+ "Pivot_P1",
+ "Pivot_P2",
+ "Pivot_P3"
+ ]
+ ],
+ [
+ "buildings/louisa_station.blend",
+ [
+ "LOUISA_Station",
+ "LOUISA_TreePoints"
+ ]
+ ],
+ [
+ "buildings/s1_mirf_buildings.blend",
+ [
+ "S1_MIRF_Buildings"
+ ]
+ ],
+ [
+ "buildings/signs/sign_S1.blend",
+ [
+ "SIGN_S1"
+ ]
+ ],
+ [
+ "scenes/S1/layers/s1_vegetation.blend",
+ [
+ "Vegetation"
+ ]
+ ]
+ ],
+ "missing": [],
+ "warnings": [],
+ "mismatch": [],
+ "scene": [
+ "scenes/S1/S1.blend",
+ 135112
+ ]
+}
\ No newline at end of file
diff --git a/tools/reports/build_s2.json b/tools/reports/build_s2.json
new file mode 100644
index 0000000..9fdcf9e
--- /dev/null
+++ b/tools/reports/build_s2.json
@@ -0,0 +1,204 @@
+{
+ "written": [
+ [
+ "plants/grasses_s2.blend",
+ [
+ "GrassVariants_S2",
+ "TallGrassVariants_S2"
+ ],
+ 282861
+ ],
+ [
+ "scenes/S2/layers/s2_terrain.blend",
+ [
+ "Terrain"
+ ],
+ 47876196
+ ],
+ [
+ "instruments/CSAPR2/CSAPR2.blend",
+ [
+ "CSAPR2"
+ ],
+ 1249303
+ ],
+ [
+ "instruments/AWOS/AWOS.blend",
+ [
+ "INST_AWOS"
+ ],
+ 262341
+ ],
+ [
+ "vehicles/c850_arm.blend",
+ [
+ "C850_ARM"
+ ],
+ 5610750
+ ],
+ [
+ "containers/sacr_container_s2.blend",
+ [
+ "CONT_7_SACR_S2"
+ ],
+ 149004
+ ],
+ [
+ "buildings/s2_kawg_buildings.blend",
+ [
+ "S2_KAWG_Buildings"
+ ],
+ 1834481
+ ],
+ [
+ "buildings/signs/sign_S2.blend",
+ [
+ "SIGN_S2"
+ ],
+ 853969
+ ],
+ [
+ "scenes/S2/layers/s2_power_roads.blend",
+ [
+ "S1_Power_Roads"
+ ],
+ 157691
+ ],
+ [
+ "scenes/S2/layers/s2_vegetation.blend",
+ [
+ "Vegetation"
+ ],
+ 40854114
+ ]
+ ],
+ "linked": [
+ [
+ "plants/crops.blend",
+ [
+ "CornVariants",
+ "SoyVariants"
+ ]
+ ],
+ [
+ "plants/grasses.blend",
+ [
+ "GrassVariants",
+ "TallGrassVariants"
+ ]
+ ],
+ [
+ "plants/grasses_s2.blend",
+ [
+ "GrassVariants_S2",
+ "TallGrassVariants_S2"
+ ]
+ ],
+ [
+ "plants/trees_hd.blend",
+ [
+ "TV_DecidBaked_HD"
+ ]
+ ],
+ [
+ "scenes/S2/layers/s2_terrain.blend",
+ [
+ "Terrain"
+ ]
+ ],
+ [
+ "instruments/CSAPR2/CSAPR2.blend",
+ [
+ "CSAPR2"
+ ]
+ ],
+ [
+ "instruments/AWOS/AWOS.blend",
+ [
+ "INST_AWOS"
+ ]
+ ],
+ [
+ "vehicles/c850_arm.blend",
+ [
+ "C850_ARM"
+ ]
+ ],
+ [
+ "containers/amf2_containers.blend",
+ [
+ "CONT_7_SACR"
+ ]
+ ],
+ [
+ "containers/sacr_container_s2.blend",
+ [
+ "CONT_7_SACR_S2"
+ ]
+ ],
+ [
+ "buildings/s2_kawg_buildings.blend",
+ [
+ "S2_KAWG_Buildings"
+ ]
+ ],
+ [
+ "buildings/signs/sign_S2.blend",
+ [
+ "SIGN_S2"
+ ]
+ ],
+ [
+ "scenes/S2/layers/s2_power_roads.blend",
+ [
+ "S1_Power_Roads"
+ ]
+ ],
+ [
+ "scenes/S2/layers/s2_vegetation.blend",
+ [
+ "Vegetation"
+ ]
+ ]
+ ],
+ "missing": [
+ [
+ "image",
+ "derechos_sign_S2.001",
+ "/Users/scollis/Documents/blender/farm_amf2/signs/derechos_sign_S2.001.png"
+ ],
+ [
+ "image",
+ "derechos_sign_S2.002",
+ "/Users/scollis/Documents/blender/farm_amf2/signs/derechos_sign_S2.002.png"
+ ]
+ ],
+ "warnings": [
+ "missing image derechos_sign_S2.001 /Users/scollis/Documents/blender/farm_amf2/signs/derechos_sign_S2.001.png",
+ "missing image derechos_sign_S2.002 /Users/scollis/Documents/blender/farm_amf2/signs/derechos_sign_S2.002.png",
+ "signature mismatch GrassVariants vs plants/grasses.blend GrassVariants",
+ "signature mismatch TallGrassVariants vs plants/grasses.blend TallGrassVariants",
+ "normalized mismatch CONT_7_SACR containers/amf2_containers.blend CONT_7_SACR"
+ ],
+ "mismatch": [
+ [
+ "GrassVariants",
+ "plants/grasses.blend",
+ "GrassVariants"
+ ],
+ [
+ "TallGrassVariants",
+ "plants/grasses.blend",
+ "TallGrassVariants"
+ ],
+ [
+ "CONT_7_SACR",
+ "containers/amf2_containers.blend",
+ "CONT_7_SACR"
+ ]
+ ],
+ "scene": [
+ "scenes/S2/S2.blend",
+ 144075
+ ]
+}
\ No newline at end of file
diff --git a/tools/reports/clouds_cm1_bomex_cumulus.json b/tools/reports/clouds_cm1_bomex_cumulus.json
new file mode 100644
index 0000000..63bd86e
--- /dev/null
+++ b/tools/reports/clouds_cm1_bomex_cumulus.json
@@ -0,0 +1,17 @@
+{
+ "written": [
+ [
+ "clouds/cm1_bomex_cumulus/cm1_bomex_cumulus.blend",
+ [
+ "CLOUD_bomex_t240",
+ "CLOUD_bomex_t300",
+ "CLOUD_bomex_t360"
+ ],
+ 66346
+ ]
+ ],
+ "linked": [],
+ "missing": [],
+ "warnings": [],
+ "mismatch": []
+}
\ No newline at end of file
diff --git a/tools/reports/clouds_cm1_supercell.json b/tools/reports/clouds_cm1_supercell.json
new file mode 100644
index 0000000..2b21e8b
--- /dev/null
+++ b/tools/reports/clouds_cm1_supercell.json
@@ -0,0 +1,19 @@
+{
+ "written": [
+ [
+ "clouds/cm1_supercell/cm1_supercell.blend",
+ [
+ "CLOUD_supercell_c100_t25",
+ "CLOUD_supercell_s100_t45",
+ "CLOUD_supercell_s100_t60",
+ "CLOUD_supercell_s250_t45",
+ "CLOUD_supercell_s250_t60"
+ ],
+ 66830
+ ]
+ ],
+ "linked": [],
+ "missing": [],
+ "warnings": [],
+ "mismatch": []
+}
\ No newline at end of file
diff --git a/tools/reports/clouds_lasso_sgp_20170403.json b/tools/reports/clouds_lasso_sgp_20170403.json
new file mode 100644
index 0000000..e6bcb67
--- /dev/null
+++ b/tools/reports/clouds_lasso_sgp_20170403.json
@@ -0,0 +1,19 @@
+{
+ "written": [
+ [
+ "clouds/lasso_sgp_20170403/lasso_sgp_20170403.blend",
+ [
+ "CLOUD_lasso_1740",
+ "CLOUD_lasso_1800",
+ "CLOUD_lasso_1900",
+ "CLOUD_lasso_2000",
+ "CLOUD_lasso8_2040"
+ ],
+ 69736
+ ]
+ ],
+ "linked": [],
+ "missing": [],
+ "warnings": [],
+ "mismatch": []
+}
\ No newline at end of file
diff --git a/tools/reports/clouds_ucla_cumulus.json b/tools/reports/clouds_ucla_cumulus.json
new file mode 100644
index 0000000..d65dd16
--- /dev/null
+++ b/tools/reports/clouds_ucla_cumulus.json
@@ -0,0 +1,15 @@
+{
+ "written": [
+ [
+ "clouds/ucla_cumulus/ucla_cumulus.blend",
+ [
+ "CLOUD_ucla_t6600"
+ ],
+ 69115
+ ]
+ ],
+ "linked": [],
+ "missing": [],
+ "warnings": [],
+ "mismatch": []
+}
\ No newline at end of file
diff --git a/tools/scene_check.py b/tools/scene_check.py
new file mode 100644
index 0000000..ab21ba9
--- /dev/null
+++ b/tools/scene_check.py
@@ -0,0 +1,50 @@
+"""Evaluate a scene and write a fingerprint (instance counts, vertex totals, transform sums)
+plus optional low-res test renders, so a rebuilt scene can be compared with its source.
+blender -b scene.blend --python tools/scene_check.py -- out_prefix [cam1,cam2] [hide_colls]
+"""
+import bpy, sys, json, os
+args = sys.argv[sys.argv.index('--') + 1:]
+out = args[0]
+cams = args[1].split(',') if len(args) > 1 and args[1] else []
+sc = bpy.context.scene
+if len(args) > 2 and args[2] == 'fiximg':
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+ import ablib
+ for img in bpy.data.images:
+ if img.library or img.packed_file or not img.filepath or img.source in ('VIEWER', 'GENERATED'):
+ continue
+ p = ablib.resolve_src(img.filepath)
+ if p: img.filepath = p
+dg = bpy.context.evaluated_depsgraph_get()
+from collections import defaultdict
+n = defaultdict(int); verts = defaultdict(int); tsum = [0.0, 0.0, 0.0]
+for inst in dg.object_instances:
+ o = inst.object
+ if not inst.show_self: continue
+ n[o.type] += 1
+ if o.type == 'MESH':
+ verts['mesh'] += len(o.data.vertices)
+ if o.type == 'EMPTY': continue
+ t = inst.matrix_world.translation
+ tsum[0] += t.x; tsum[1] += t.y; tsum[2] += t.z
+fp = {'instances': dict(n), 'verts': dict(verts), 'tsum': [round(v, 1) for v in tsum],
+ 'libs': [l.filepath for l in bpy.data.libraries],
+ 'missing_libs': [l.filepath for l in bpy.data.libraries if not os.path.exists(bpy.path.abspath(l.filepath))]}
+json.dump(fp, open(out + '_fp.json', 'w'), indent=1)
+r = sc.render
+r.engine = 'CYCLES'; sc.cycles.samples = 16; sc.cycles.seed = 0; sc.cycles.use_denoising = False
+r.resolution_x, r.resolution_y, r.resolution_percentage = 640, 360, 100
+r.image_settings.file_format = 'PNG'
+try:
+ prefs = bpy.context.preferences.addons['cycles'].preferences
+ prefs.compute_device_type = 'METAL'; prefs.get_devices()
+ for d in prefs.devices: d.use = True
+ sc.cycles.device = 'GPU'
+except Exception as e:
+ print('gpu setup failed', e)
+for c in cams:
+ if c not in bpy.data.objects: print('no camera', c); continue
+ sc.camera = bpy.data.objects[c]
+ r.filepath = f'{out}_{c}.png'
+ bpy.ops.render.render(write_still=True)
+print('CHECK_DONE')
diff --git a/vehicles/README.md b/vehicles/README.md
new file mode 100644
index 0000000..ea5d4c0
--- /dev/null
+++ b/vehicles/README.md
@@ -0,0 +1,14 @@
+# Vehicles
+
+Farm vehicles/implements and the ARM Bombardier Challenger 850 (`C850_ARM`, gear/flaps/fans driven from custom properties on `C850_root`).
+
+| Preview | Collection | File |
+|---|---|---|
+|
| `VEH_Tractor_Green` | [`farm_vehicles.blend`](farm_vehicles.blend) (1.0 MB) |
+|
| `VEH_Telehandler_Yellow` | [`farm_vehicles.blend`](farm_vehicles.blend) (1.0 MB) |
+|
| `VEH_Pickup_White` | [`farm_vehicles.blend`](farm_vehicles.blend) (1.0 MB) |
+|
| `VEH_Implement_Disk_Green` | [`farm_vehicles.blend`](farm_vehicles.blend) (1.0 MB) |
+|
| `VEH_GrainCart_Red` | [`farm_vehicles.blend`](farm_vehicles.blend) (1.0 MB) |
+|
| `VEH_Gooseneck_Flatbed` | [`farm_vehicles.blend`](farm_vehicles.blend) (1.0 MB) |
+|
| `VEH_Cultivator_Red` | [`farm_vehicles.blend`](farm_vehicles.blend) (1.0 MB) |
+|
| `C850_ARM` | [`c850_arm.blend`](c850_arm.blend) (5.6 MB) |
diff --git a/vehicles/c850_arm.blend b/vehicles/c850_arm.blend
new file mode 100644
index 0000000..1660be2
Binary files /dev/null and b/vehicles/c850_arm.blend differ
diff --git a/vehicles/farm_vehicles.blend b/vehicles/farm_vehicles.blend
new file mode 100644
index 0000000..d2bc0f4
Binary files /dev/null and b/vehicles/farm_vehicles.blend differ
diff --git a/vehicles/preview/C850_ARM.jpg b/vehicles/preview/C850_ARM.jpg
new file mode 100644
index 0000000..2e5626a
Binary files /dev/null and b/vehicles/preview/C850_ARM.jpg differ
diff --git a/vehicles/preview/VEH_Cultivator_Red.jpg b/vehicles/preview/VEH_Cultivator_Red.jpg
new file mode 100644
index 0000000..8bc179f
Binary files /dev/null and b/vehicles/preview/VEH_Cultivator_Red.jpg differ
diff --git a/vehicles/preview/VEH_Gooseneck_Flatbed.jpg b/vehicles/preview/VEH_Gooseneck_Flatbed.jpg
new file mode 100644
index 0000000..e6c0970
Binary files /dev/null and b/vehicles/preview/VEH_Gooseneck_Flatbed.jpg differ
diff --git a/vehicles/preview/VEH_GrainCart_Red.jpg b/vehicles/preview/VEH_GrainCart_Red.jpg
new file mode 100644
index 0000000..3745767
Binary files /dev/null and b/vehicles/preview/VEH_GrainCart_Red.jpg differ
diff --git a/vehicles/preview/VEH_Implement_Disk_Green.jpg b/vehicles/preview/VEH_Implement_Disk_Green.jpg
new file mode 100644
index 0000000..6f96cd8
Binary files /dev/null and b/vehicles/preview/VEH_Implement_Disk_Green.jpg differ
diff --git a/vehicles/preview/VEH_Pickup_White.jpg b/vehicles/preview/VEH_Pickup_White.jpg
new file mode 100644
index 0000000..26b3345
Binary files /dev/null and b/vehicles/preview/VEH_Pickup_White.jpg differ
diff --git a/vehicles/preview/VEH_Telehandler_Yellow.jpg b/vehicles/preview/VEH_Telehandler_Yellow.jpg
new file mode 100644
index 0000000..29e4985
Binary files /dev/null and b/vehicles/preview/VEH_Telehandler_Yellow.jpg differ
diff --git a/vehicles/preview/VEH_Tractor_Green.jpg b/vehicles/preview/VEH_Tractor_Green.jpg
new file mode 100644
index 0000000..e9f5f26
Binary files /dev/null and b/vehicles/preview/VEH_Tractor_Green.jpg differ