bevy_elements 0.7.2

Living environments for Bevy: sun, moon and stars, climate-driven weather, GPU wind, volumetric clouds, rain, snow and fog, lightning, terrain with hydraulic and thermal erosion, flooding, snow cover, wetness and dust — simulated in compute shaders.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
4685
4686
4687
4688
4689
4690
4691
4692
4693
4694
4695
4696
4697
4698
4699
4700
4701
4702
4703
4704
4705
4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
4735
4736
4737
4738
4739
4740
4741
4742
4743
4744
4745
4746
4747
4748
4749
4750
4751
4752
4753
4754
4755
4756
4757
4758
4759
4760
4761
4762
4763
4764
4765
4766
4767
4768
4769
4770
4771
4772
4773
4774
4775
4776
4777
4778
4779
4780
4781
4782
4783
4784
4785
4786
4787
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
4808
4809
4810
4811
4812
4813
4814
4815
4816
4817
4818
4819
4820
4821
4822
4823
4824
4825
4826
4827
4828
4829
4830
4831
4832
4833
4834
4835
4836
4837
4838
4839
4840
4841
4842
4843
4844
4845
4846
4847
4848
4849
4850
4851
4852
4853
4854
4855
4856
4857
4858
4859
4860
4861
4862
4863
4864
4865
4866
4867
4868
4869
4870
4871
4872
4873
4874
4875
4876
4877
4878
4879
4880
4881
4882
4883
4884
4885
4886
4887
4888
4889
4890
4891
4892
4893
4894
4895
4896
4897
4898
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
4909
4910
4911
4912
4913
4914
4915
4916
4917
4918
4919
4920
4921
4922
4923
4924
4925
4926
4927
4928
4929
4930
4931
4932
4933
4934
4935
4936
4937
4938
4939
4940
4941
4942
4943
4944
4945
4946
4947
4948
4949
4950
4951
4952
4953
4954
4955
4956
4957
4958
4959
4960
4961
4962
4963
4964
4965
4966
4967
4968
4969
4970
4971
4972
4973
4974
4975
4976
4977
4978
4979
4980
4981
4982
4983
4984
4985
4986
4987
4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
5000
5001
5002
5003
5004
5005
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
5029
5030
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
5050
5051
5052
5053
5054
5055
5056
5057
5058
5059
5060
5061
5062
5063
5064
5065
5066
5067
5068
5069
5070
5071
5072
5073
5074
5075
5076
5077
5078
5079
5080
5081
5082
5083
5084
5085
5086
5087
5088
5089
5090
5091
5092
5093
5094
5095
5096
5097
5098
5099
5100
5101
5102
5103
5104
5105
5106
5107
5108
5109
5110
5111
5112
5113
5114
5115
5116
5117
5118
5119
5120
5121
5122
5123
5124
5125
5126
5127
5128
5129
5130
5131
5132
5133
5134
5135
5136
5137
5138
5139
5140
5141
5142
5143
5144
5145
5146
5147
5148
5149
5150
5151
5152
5153
5154
5155
5156
5157
5158
5159
5160
5161
5162
5163
5164
5165
5166
5167
5168
5169
5170
5171
5172
5173
5174
5175
5176
5177
5178
5179
5180
5181
5182
5183
5184
5185
5186
5187
5188
5189
5190
5191
5192
5193
5194
5195
5196
5197
5198
5199
5200
5201
5202
5203
5204
5205
5206
5207
5208
5209
5210
5211
5212
5213
5214
5215
5216
5217
5218
5219
5220
5221
5222
5223
5224
5225
5226
5227
5228
5229
5230
5231
5232
5233
5234
5235
5236
5237
5238
5239
5240
5241
5242
5243
5244
5245
5246
5247
5248
5249
5250
5251
5252
5253
5254
5255
5256
5257
5258
5259
5260
5261
5262
5263
5264
5265
5266
5267
5268
5269
5270
5271
5272
5273
5274
5275
5276
5277
5278
5279
5280
5281
5282
5283
5284
5285
5286
5287
5288
5289
5290
5291
5292
5293
5294
5295
5296
5297
5298
5299
5300
5301
5302
5303
5304
5305
5306
5307
5308
5309
5310
5311
5312
5313
5314
5315
5316
5317
5318
5319
5320
5321
5322
5323
5324
5325
5326
5327
5328
5329
5330
5331
5332
5333
5334
5335
5336
5337
5338
5339
5340
5341
5342
5343
5344
5345
5346
5347
5348
5349
5350
5351
5352
5353
5354
5355
# bevy_elements improvement loop — log

One improvement per iteration: captured, inspected, picked, researched, implemented, gated, shipped.
Iterations are appended newest-last.

## Iteration 1 — 2026-10-14 — the ground-fog volume darkened the sky it could not light

**Commit:** (this iteration) · **Sibling repos:** none touched.

### What changed, and why it ranked first

`FogPlugin`'s camera-following ground-fog `FogVolume` now fades out where Bevy cannot light it, and
carries the scene's *total* light where it can.

Bevy lights a `FogVolume` from the `DirectionalLight`s alone, and under cloud those carry only the
beam `T̄` — at full cover the sun's illuminance is literally 0 (instrumented: `beam = 0.0000`). The
cloud-scattered diffuse light reaches the camera's `AmbientLight` and never the volume, and Bevy's
`VolumetricFog.ambient_*` term cannot be the missing light (it is added once per ray, with no
density texture and no exposure — Bevy's own docs and this crate's note say so, which is why the
camera sets `ambient_intensity = 0`). So a thick cloud plus a short visibility made the volume
*remove* the sky and return nothing: a blizzard at noon rendered as a dark hole.

Adopted: `volume.density_factor = T̄ / (T̄ + 0.25 (1 − T̄))` and
`volume.light_intensity *= (T̄ + 0.25 (1 − T̄)) / T̄`, clamped 1–100. The two are exact reciprocals, so
the volume's radiance over its opacity is the scene's total light at every cover: it keeps its
appearance wherever it is lit and simply fades out of the frame where Bevy cannot light it, leaving
the sky and the distance fog to draw the whiteout. Ranked first because it is a **physically wrong
result visible in a shipping game's worst weather** (rank 1), not polish: an unlit volume that
occludes is an energy violation, and it is the most visible of the candidates at any hour.

Runner-up: the harness had no deterministic frame-sequence capture, which every later iteration
needs. It was built anyway as part of step 2 (below), but the fog defect outranked it as the
iteration's subject.

### Inspection findings

Captured before the change (`/tmp/i1/before/`, `/tmp/i1/ctl/`) and after (`/tmp/i1/after/`,
`/tmp/i1/fixed/`): a time-of-day sweep (05:30, 12:00, 20:12, 23:30) and all ten presets at noon,
1280 × 720, via `weather_tour`. The vision reviewer's crops are in `/tmp/i1/rev/`, `/tmp/i1/rev3/`,
`/tmp/i1/rev4/`.

Two reviews ran (both through the vision model; see "Harness" below):

1. **All ten presets at noon** — ranked Blizzard worst: "sky renders as a pitch-black nighttime
   celestial sphere" at `(20, 22, 26)` with a green, unwhitened ground; Snow's ground never whitens;
   DustStorm keeps a Rayleigh-blue zenith. It also flagged a bright horizon gap under Overcast
   (measured below as pre-existing) and particle-box edges.
2. **Blizzard and Fog, before vs after** (the crops that carry this iteration) — verdict "decisive
   improvement" for both. Blizzard: "the scene now unambiguously reads as daytime overcast rather
   than an anomalous midnight snowstorm under a 56.6° noon sun."

Measured, not just judged (luminance at x=1000, like-for-like captures with and without the fix):

| Preset | visibility | before | after | note |
|---|---|---|---|---|
| Blizzard | 200 m | 59, 43 | 135, 153 | the defect: 4× darker than Clear, now brighter than Snow |
| Fog | 300 m | 179, **98** | 215, **146** | the `y=200` dip is the volume's dark band |
| Snow | 1500 m | 137, 146 | 154, 162 | lit anyway: small change |
| Thunderstorm | 1500 m | 119, 135 | 135, 152 | |
| Rain | 3000 m | 145, 153 | 151, 160 | |
| Overcast | 20 km | 153, 161 | 154, 161 | |
| Clear | 40 km | 190, 201 | 190, 201 | **unchanged**, as designed |
| DustStorm | 800 m | 115, 141 | 115, 141 | **unchanged**: its cloud is thin, so the beam is 1 |

Decisive control: with the volume's density forced to `0` and nothing else changed, Blizzard's noon
zenith read `(122, 124, 129)` against `(46, 47, 50)` with the volume — proving the volume, not the
clouds or the exposure, was eating the sky.

### Verification

- `cargo test` — 209 lib + all suites pass (2 new unit tests: the fade-out and the reciprocity).
- GPU tests `cargo test --release --tests -- --ignored --test-threads=1` — pass.
- `cargo clippy --all-targets --all-features -- -D warnings` — clean.
- `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features` — clean.
- `cargo build --lib --target wasm32-unknown-unknown` — clean.
- `cargo build --examples --all-features` — clean.
- Visual: before/after crops at the same zoom, reviewed by the vision model (above). No perf change:
  the volume is the same size and march; only two scalars per frame differ.

### Harness

Two problems in step 2 were fixed, because every later iteration needs them:

1. **No frame-sequence capture.** `examples/common/mod.rs` now takes `ELEMENTS_SHOT_DIR`,
   `ELEMENTS_SHOT_EVERY` and `ELEMENTS_SHOT_FROM`; it waits for the frames to be written before
   exiting and captures until killed when there is no `ELEMENTS_FRAMES`. With `ELEMENTS_FRAMES` the
   run is a fixed 1/60 s step, so two runs simulate the same time (verified: identical simulated
   state; GPU output is not bit-deterministic, which the docs now say).
2. **Vision subagents fail on OpenRouter's `/responses` path.** A subagent that reads an image with
   a tool and then takes another turn gets `400 "Requests ending with a model turn are not
   supported"` from Google — reproduced and narrowed by replaying the exact request body: it is the
   replayed `reasoning` items. `PI_OPENROUTER_RESPONSES=0` (Chat Completions) accepts the same
   history; `/tmp/i1/vision.sh` runs a review that way. The in-process `modelRoles.vision` route is
   unusable for tool-using image review until this is fixed upstream.

### Visual review (`screenshot-reviewer`, after-crops, `/tmp/i5/review_a.md`)

The reviewer compared before/after at the same zoom levels and called the horizon finding
**resolved**: before, a 67 DN dip (sky L=227, shelf floor L=160, ground L=176) at `y = 85..104`
across the whole width; after, a monotone ramp (L=226 → 220 → 179 → 176) with **0 DN** of dip, on
both `Overcast` and `Rain`. It confirmed the ground now reads as snow-covered and matches the
weathered columns (`srgb(228, 229, 232)`), and that the 40 km plane shows no vertex swimming,
z-fighting or edge clipping against the atmosphere probe.

One **Noticeable** finding stands: the flat harness's snow ground is a uniform wash (std dev
< 0.4 DN, no relief). Cause measured, and it is a property of the scene rather than a regression:
the shipped snow path perturbs only `base_color`, `roughness` and `metallic` (`mirror.rs:149-151`)
and never the normal, so an up-facing plane lit only by the cloud-dimmed beam has nothing to vary
it — under Overcast the directional light's illuminance is near zero, so there are no cast
shadows either. On real terrain the same path follows the relief: `terrain_snow_g24` has a ground
std dev of 16 (mean 222) against the flat plane's 0, and the snow lies on the ridges. A snow
normal/albedo perturbation would be a new system, and so a research note first.

A second focused review (`/tmp/i5/review_b.md`) checked regressions and found **none**: Clear
ground identical (RMSE 3.9e-05, sampled (140,155,113) before and after) and PartlyCloudy unchanged
within dither (RMSE 7.56e-04, (132,146,112) → (132,146,113), cloud shadows matching), the 40 km
plane integrated to the horizon with no seam or step on any preset, DustStorm's ochre veil and
Fog's extinction uniform with no halos, dusk banding-free, night ground exactly (0,0,0) with
pin-sharp stars.

### Top 5 remaining items, ranked

1. **Blizzard's whiteout is not white yet.** Measured after the fix: sky 135–185 against snow 225,
   so the horizon line stays visible and a distant pillar reads with `ΔL ≈ 35 %` against the sky.
   The distance fog's in-scatter at maximum depth is brighter than the horizon sky. Rank 1: it is
   the same class of physically wrong result as this iteration's, in the same preset.
2. **A bright horizon gap / step under Overcast and Rain** (`y ≈ 270`, ~30 DN). Pre-existing
   (measured 36 DN before, 29 DN after this fix — it got slightly *smaller*), so not a regression;
   the reviewer found it in both passes. Distance-fog colour against the sky above the horizon.
3. **Snow's ground never whitens** under Snow or Blizzard in `weather_tour` (the ground-cover maps
   need terrain; the example's flat scene uses the plain ground material). Needs a decision:
   document it, or give the harness's ground plane the cover shading.
4. **DustStorm keeps a Rayleigh-blue zenith** with pristine white cumulus at 800 m visibility.
   The fog tint is only in the distance fog; the sky above is untouched.
5. **Particle-box edge** at 60 m under Rain/Thunderstorm: streaks stop abruptly against distant
   un-occluded geometry, and the draw has no far-particle haze.

Deferred/known, not ranked: the per-preset `docs/*.png` stills and `docs/tour.gif` were regenerated
this iteration only where the shipped build changed them.

## Iteration 3 — 2026-10-14 — the sky carries the weather's air (the whiteout's horizon line)

**Commit:** 816f473 · **Sibling repos:** none touched.

### What changed, and why it ranked first

The cloud composite now fogs **sky** pixels by the same air the distance fog gives every mesh:
`σ_air = σ_fog − 3.912/40 km`, read from the camera's own `DistanceFog` (`fog::sky_air_extinction`,
`CloudParams::air`, `composite.wesl`), multiplying the deck's radiance by `e^(−σ_air·t)` and adding
the fog colour times the same share. `t` is the path to the layer's entry, the conservative limit
(the veil beyond converges on the same colour, Validation 20).

Ranked first because it is the **rank-1 backlog item from iteration 2 and the top of iteration 1's
list**: a physically wrong result in the shipping crate's worst weather. The fog made the ground
converge on the fog colour but left the sky at the deck's own radiance, so a 200 m-visibility
blizzard showed a 40-level luminance step at the horizon — measured, and named "Blocker / inverted
luminance" by iteration 2's visual review.

Runner-up: snow's ground never whitens in `weather_tour` (backlog 3). It needs a terrain and the
ground-cover maps, so it is a harness change, not a renderer defect; the horizon step is visible in
every whiteout image in every scene.

### Inspection findings (iteration 2's captures, `/tmp/i2/`, and this iteration's `/tmp/i3/before/`)

Measured on the before build at the horizon (`x = 1000`): `tour_Blizzard.png` sky `(186, 193, 198)`
at `y = 269` against ground `(224, 228, 230)` at `y = 270`; `terrain_blizzard_air.png` sky
`(185, 188, 191)` at `y = 297` against terrain `(225, 228, 231)` at `y = 298`. `tour_Fog.png` had a
dark cloud band `(144, 147, 154)` between a bright fog base and the upper sky. Crops and the
before/after review are in `/tmp/i3/rev/`.

### Verification

Measured on the before/after captures at the horizon (`x = 1000`, mean of RGB), the step the
review called a blocker:

| scene | sky just above it | ground just below it | after |
|---|---|---|---|
| `tour_Blizzard` (200 m) | 192 | 225 | **227 / 225** — step gone |
| `tour_Fog` (300 m) | 191 (mid-sky band 148) | 216 | **219 / 216** — the dark band is gone |
| `tour_Snow` (1500 m) | 192 | 222 | **227 / 222** |
| `tour_DustStorm` (800 m) | 188 (chromatic clash) | 194 | **198 / 194**, the sky takes the ochre tint |
| `tour_Clear` (40 km) | — | — | **bit-identical, AE = 0** (also dawn, dusk, night) |
| `terrain_blizzard_g` | 175 | 177 | **227 / 227** |

The vision review's crops are in `/tmp/i3/rev/`; its verdict: "Fit to ship: horizon seam is
completely eliminated under low visibility while clear conditions remain bit-exact", with the
Blizzard horizon crop's standard deviation down from 4255.7 to 802.6 and `terrain_blizzard_g`'s
from 7565.6 to 186.6.

- `cargo test` — 211 lib + all suites pass (2 new unit tests: the air's arithmetic and monotonicity;
  the `Clear` preset pinned to `CLEAR_AIR_EXTINCTION`); `tour.gif` and the per-preset stills are
  regenerated from the new build and re-reviewed (no artifacts, all presets distinct).
- New GPU test `the_weather_air_makes_the_sky_the_fog_colour`: straight-up Blizzard, linear target,
  the frame's mean equals the fog colour to a half-float step (Δ 0.0026 of 6.91) with the air on and
  differs from it by half its luminance with the air suppressed (`CloudDebug::air = 0`).
- `the_far_veil_continues_the_marched_deck_at_full_cover` (Validation 20) now suppresses the air
  explicitly, because Deviation 29's fog saturated both sides of its 32 km boundary (ratio 1.000)
  and the test had gone blind; with the air off its measured ratios are 1.04–1.13 — the same values
  the build before Deviation 29 produced.
- `cargo clippy --all-targets --all-features -- -D warnings`, `RUSTDOCFLAGS="-D warnings" cargo doc
  --no-deps --all-features`, `cargo build --lib --target wasm32-unknown-unknown` — see the gate run
  below.

### Top 5 remaining items, ranked

1. **Snow's ground never whitens** under Snow or Blizzard in `weather_tour` (backlog 3 from
   iteration 1). The harness's flat ground uses the plain material; giving it the ground-cover
   shading (or moving the whiteout shots to `terrain_tour`) would let every later whiteout claim be
   judged on a scene that can actually be white.
2. **DustStorm keeps a Rayleigh-blue zenith** (backlog 4) with white cumulus at 800 m visibility:
   the fog tint is only in the distance fog; the sky above is untouched. The air mechanism added
   this iteration is the hook: the dust presets' *colour*, not just their extinction, is what is
   missing (`fog_hue` already tints the distance fog for dust).
3. **Particle-box edge** at 60 m under Rain/Thunderstorm (backlog 5): streaks stop abruptly against
   distant un-occluded geometry, and the draw has no far-particle haze.
4. **A bright horizon gap / step under Overcast and Rain** (~30 DN at `y ≈ 270`, iteration 1's
   backlog 2): pre-existing before iteration 1 and only slightly reduced by it; the air added this
   iteration changes it (Overcast's air is 9.78×10⁻⁵ m⁻¹), so re-measure it against these captures.
5. **Cloud edge stair-stepping** (iteration 2's review 4): 1–3 px discrete steps along cloud
   silhouettes in `tour_Clear`, `tour_Fog` and `tour_DustStorm`; a filtering or noise-octave
   question, not an energy one.

## Iteration 4 — 2026-10-15 — snow lies on the ground the examples show

**Commit:** `177754b` · **Sibling repos:** none touched.

### What changed, and why it ranked first

The examples' flat ground plane was a bespoke `StandardMaterial` extension that sampled only the
cloud shadow map (`examples/common/ground.rs` + `ground.wesl`, since deleted), so under `Snow`
and `Blizzard` `weather_tour` rendered olive-drab ground where the boxes and columns beside it
carried `WeatheredMaterial` and whitened. Two layers, one fix.

1. **Material.** `test_scene` now gives its plane `WeatheredMaterial` (the crate's own), so the
   example ground darkens and glosses in rain, runs with streaks and whitens under snow exactly as
   a consumer's mesh does. The duplicate shader is gone.
2. **Plane extent.** 2 km → 40 km. A 2 km plane leaves a sliver of unlit below-horizon
   atmosphere between its edge (1 km away) and the horizon; `ground_haze` (clouds.md Deviation 28)
   paints the camera's distance fog over it, and the result is a dark shelf at the horizon.
3. **Capture.** `SurfaceWeathering` and the cover maps advance on *game time*, and `ELEMENTS_HOUR`
   holds the clock, so a still of the first frame integrates 0 h of the held preset: no material
   could ever whiten in a screenshot. `ELEMENTS_GROW_HOURS=n` (examples/common, `grow_weather`)
   advances the clock one hour a frame for `n` frames plus one arming frame, then rewinds by what it
   advanced. Both consumers record the clock on their first frame and integrate nothing from it, so
   the arming frame is needed; and a jump larger than 24 h is clamped by both, so the growth is
   split into one-hour chunks. The captured hour, sun and sky stay the example's own.

Why it ranked first: rank 1 on iterations 1 and 3's backlogs, and the reviewer's finding 3
("ground under `tour_Snow` fails to read as snow-covered"). It beats the runner-up (iteration 3's
backlog item 2, the bright horizon step under Overcast and Rain) because the horizon step is
*physically* the fogged ground against a bright deck and moves only 29 DN, while a snowing sky over
green grass is a wrong result the crate can fix.

### Inspection findings

Before/after pairs are in `/tmp/i4/before/`, `/tmp/i4/after/`, crops in `/tmp/i4/crops/` and
`/tmp/i4/crops_after/`. The reviewer's before findings: `rain_sky` streaks at Δ ≤ 8 RGB levels
(versus > 50 against ground); `snow_ground` olive-drab; `overcast_horizon` a 66 DN drop (sky
L=225) to a band floor of L=159, 16 DN below the ground it meets. Measured on the new build,
x=16 column, rows 264-280 (`magick -crop 1x17+16+264`):

| scene | before | after |
|---|---|---|
| `tour_Overcast` horizon shelf | 224→194→156 (valley) →173 | 224→220→203→194→…→173 (ramp) |
| `tour_PartlyCloudy` horizon | 219→182→155 (valley) →196 | 219→211→204→201→…→196 |
| `tour_Clear` horizon | 203→207→170 (step) | 203→209→193→…→170 |

The 66 DN valley under Overcast and PartlyCloudy is gone: after the fix the same column is a
monotone ramp (224→203→194→186→181→177→173) with no dip below the ground it meets. Clear's
30 DN step is unchanged (clear air has no fog to converge on, and the Clear preset's plane edge
was never the cause).

### Verification

- `cargo test`: 242 tests, **211 pass, 31 ignored** (GPU, not run), 0 fail.
- `cargo clippy --all-targets --all-features -- -D warnings`: clean.
- `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features`: clean.
- `cargo build --lib --target wasm32-unknown-unknown`: clean.
- GPU tests, one suite per process: `clouds::` 10 pass, `terrain::` 8 pass, `cover::` 5 pass /
  **2 fail (pre-existing)**, `dust::` 6 pass, `voxel::` **1 fail (pre-existing)**, integration
  (`tests/gpu_*`) 28 pass. The two cover failures (`gpu_wetness_soaks_and_dries_on_the_formula`,
  probe `Some(0.992)` against an expected `< 0.1` after drying, and
  `gpu_surface_weathering_follows_the_map`) were reproduced on **pristine HEAD** with this work
  stashed — a wetness-tolerance defect in the cover map, not a regression. The voxel failure
  (`gpu_voxel_snow_covers_the_top`, `src/voxel/gpu_tests.rs:318`) reproduces 2/2 at pristine HEAD
  with swap 1 at 23.05 h and 22.80 h against `20.7 ± 2.0 h` — a ~+2.1 h bias past the window, with
  no simulation-side `src/` drift since the test was written (the only later edits are a type
  alias and a constant rename). Both are logged below as backlog item 2.
- Ground material, snow and growth, before → after: `tour_Snow` (155,167,141) → (228,229,232) at 6 h;
  `tour_Blizzard` (155,167,141) → (228,229,232); `terrain_snow_g` (143,162,132) → (228,230,232) at 24 h
  (`CoverStats::simulated_s` = 86 400 s exactly, `terrain_blizzard_g` same).
- Determinism: two `ELEMENTS_GROW_HOURS=6` runs give identical ground (228,230,232) and identical
  status-line hour pixels (158,158) — the warm-up is reproducible.
- Regression: `ELEMENTS_GROW_HOURS=0` is bit-identical to the build before this change. Clear
  ground (140,155,112) before and after; PartlyCloudy ground (132,146,112) before and after.
- Cost: the change is `examples/`-only plus one stale `src/terrain/render.wesl` comment; no shader,
  kernel, or uniform changed, so the cost tables in the notes stand.

### Top 5 remaining items, ranked

1. **Snow has no surface relief on a flat ground.** The shipped path (`mirror.rs:149-151`) moves
   `base_color`, `roughness` and `metallic` and never the normal, so on the harness's flat plane
   under Overcast (beam ≈ 0, no cast shadows) the snow is a uniform wash (std dev < 0.4 DN).
   Real terrain follows the relief (std dev 16 on `terrain_snow_g24`), so this is the flat plane's
   missing normal detail, not a bug. A snow normal perturbation needs a research note and corpus
   search first — `snow-cover.md` cites no crust source.
2. **Three GPU tests fail on pristine HEAD.** `gpu_wetness_soaks_and_dries_on_the_formula`
   (probe `Some(0.992)` vs expected `< 0.1` after drying at `src/cover/gpu_tests.rs:783`) and
   `gpu_surface_weathering_follows_the_map` (`:846`) both fail without this work — the wetness map
   reaches 0.99 after the test's drying window, so either the drying constant or the assertion's
   tolerance is wrong. `voxel::gpu_tests::gpu_voxel_snow_covers_the_top`
   (`src/voxel/gpu_tests.rs:318`) fails 2/2 with swap 1 at 22.8–23.1 h against `20.7 ± 2.0 h`: a
   consistent ~+2.1 h bias with 0.25 h run-to-run variance and no simulation-side `src/` drift
   since the test was written, so either the swap is genuinely late or the window is tighter than
   the readback-observed swap time allows. Together the only things blocking a clean
   `cargo test --release --lib -- --ignored`; a real defect in a shipped domain.
3. **The horizon step under Clear** (~30 DN at `y ≈ 270`): physically the fogged ground against a
   bright deck; the only remaining horizon discontinuity in the showcases, and the reviewer's
   before-review found it under every long-visibility preset.
4. **DustStorm keeps a Rayleigh-blue zenith** with white cumulus at 800 m visibility (backlog 4).
5. **Rain streaks wash out against the bright sky** (reviewer finding 2: Δ ≤ 8 levels against the
   overcast deck, > 50 against the ground). A contrast question in the precipitation draw.

## Iteration 5 — 2026-10-15 — the horizon shows no dark band past the edge of a finite ground

**Commit:** `c9131e6` (code + test + docs + stills). **Sibling repos:** none touched.

### What changed, and why it ranked first

The extra instruction for this run was a full aesthetic inspection: every domain the crate ships,
captured and reviewed by the vision model (`screenshot-reviewer`, Gemini Flash), then the single
most valuable defect fixed.

Below-horizon sky pixels with no mesh behind them — past the edge of quickstart's hills, the lake
basin, any finite terrain — drew Bevy's unlit bare-planet term. `ground_haze` (clouds.md
Deviation 28) fogs over the planet's own root, and from a low camera that root is *near* (23 m at
5° below the horizon from 2 m), so the transmittance is ~1 and nothing fogs: the pixel is the dark
planet under a lit, fogged scene. Measured on the lake at three columns: a 33 DN dark band at the
horizon (193 against fogged ground 226); reproduced headlessly in the new GPU test at 0.59× the
fog colour (3.835 against haze 6.553 on the linear target). The fix floors the fog distance at
the march range (`MAX_MARCH_M`, 32 km): a below-horizon pixel with no mesh shows ground the fog
has fully carried, because nothing nearer than the far field is visible beyond a finite terrain.
It beat the runner-up (precipitation and dust washing out against bright backgrounds) because it
is a physically-wrong rank-1 artifact isolated to one shader line with a regression test, while
the washout is a particle-compositing redesign that touches every precipitation golden.

### Inspection findings

Capture: `/tmp/i6` — 35 sequential runs over every example domain (`capture.sh`): the five
times of day, ten sequences (cloud build, rain, snow, lightning-lead, wind, lake ripples, flood
flow, dust, fog roll), two terrain seeds (42, 7), two camera heights (6 m and 120–400 m), erosion
before/after, voxel snow, weathered town (rain wide + wall, snow, fog). Reviews: eleven
Gemini Flash batches (`/tmp/i6/reviews/b1.md` … `b11.md`), crops in `/tmp/i6/crops/`.

- **Blocker, reclassified: the "dark horizon shelf" of iteration 4's backlog was the harness's
  own status line.** The reviewer's b6 finding (68 DN, rows 10–56 of `cover_rain_gnd`) is the
  semi-transparent debug band the harness burns into the top of every capture; b11 read it
  verbatim ("Clear (forced) … vis 40000 m") and confirmed no shelf exists in the sky below it
  (row-to-row ≤ 1–2 DN). Crop below row 56 when judging horizons; AGENTS now says so.
- **Precipitation and dust wash out against bright backgrounds** (the runner-up, now rank 1):
  rain streaks ΔDN ≤ 3 against a 227-DN overcast sky while Δ64–75 against the ground
  (`b3_*`); snow flakes Δ ≤ 5 against the sky, Δ36–44 against ground (`b4s_*`); 60 000 dust
  sprites move the frame by ≤ 4 DN over 14 frames — "the storm appears as static homogeneous
  fog" (`b5_dust_*`). Streak advection, flake motion and shelter occlusion are otherwise clean.
- **Snow has no normal-dependent shading under an overcast sky** (`b7_snow_seed07_aerial.png`):
  the 24 h warm-up saturates the terrain white but the aerial view measures 0.56 DN of standard
  dev across 840 k pixels — Bevy's ambient is flat over normals and the beam is zero. This is
  iteration 4's backlog item 1 seen from the air, on real terrain.
- **Time of day** (`b1_*`): the horizon is continuous at all four hours; the dawn gradient is
  smooth to ≤ 1 DN per row; stars are 1–2 px points; night ground sits at (4,4,5). Two
  capture-design facts: `day_night`'s camera (yaw 106.7°) never faces the sun (dawn azimuth 64°,
  sunset 303°), and the moon is set at 23:30 (−12.3°) — the showcase cannot show either disc.
  The night sky reading brighter than dusk (16.1% vs 12.5% mean) is the Ward/Larson auto
  exposure the note adopts (celestial.md, "Auto exposure"), not a defect.
- **Clouds** (`b2_*`): cumulus edges monotone, no ghosting between consecutive accumulation
  frames (Δ ≤ 1 DN), overcast deck uniform. The shadow-contrast question is unresolved (the
  crop missed the cloud mass at Clear's 0.05 cover).
- **Lightning, wind, water, materials, fog** (`b4`, `b5`, `b9`, `b10`, `b6`): bolt geometry
  clean, tree sway anchored and coherent, lake shoreline sub-pixel continuous with no shimmer,
  ripple motion structured, flood channel flush with no gaps, wall streaks localized with clean
  onset, pavilion dry contrast sharp, fog volume against geometry natural. Minor: mid-flash is
  dimmer than the first stroke because `STRIKE_LEAD=6` (+100 ms) outlives the ~43 ms decay
  (the doc says "inside the flash"); splash rings sparse; near-shore foam sub-pixel at nadir;
  the flood marker sits outside the capture's frustum; fresh erosion cuts facet; the status text
  clips at the window edge.
- **Verified physical, not defects:** thin snow lets grass through (`cover_snow_gnd` reports
  7.7 mm and melting); no puddles at 1.6 mm of water (the model's threshold is 2 mm); sequence
  frames 0–12 render before the pipelines are ready (skip them when cropping pairs).

### Verification

- New GPU test `clouds::gpu_tests::the_horizon_sky_is_the_fog_colour_under_a_deck` (6 m eye,
  1° up, `Snow`): above the horizon the veil is the fog colour (6.543 against haze 6.553); below
  it the planet was 0.59× the fog colour and is now the haze. Red before the fix, green after.
- Before/after captures (`/tmp/i6/shots` vs `/tmp/i6/after`, reviewed in `b11.md`): the lake's
  horizon band lifts 193 → 226 DN at x = 100, 1400, 1800 with meshed terrain bit-identical
  (Δ 0 DN); `weathered_town`'s 40 km plane compares AE 0; flood fringes converge into the
  fogged ridge; open sky Δ 1 DN. The controls `tod_noon` (AE 1.4e-8) and the terrain seed-42
  ground shot (AE 2.4e-5, dither) are unchanged.
- `cargo test`: 211 pass, 0 fail (32 ignored, GPU). `cargo clippy --all-targets --all-features
  -- -D warnings`, `RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features`, and the
  wasm32 build: clean.
- GPU suites, one process per module: `clouds::` 11 pass in two parts (see below), `terrain::`
  8 pass, `dust::` 6 pass, integration (`tests/gpu_*`) 28 pass; `cover::` and `voxel::` fail
  exactly the three pre-existing tests logged under iteration 4's backlog item 2 — reproduced
  there first, unchanged here.
- New test-infrastructure fact: `clouds::gpu_tests` now has eleven tests and the eleventh
  sequential wgpu instance cannot get an adapter (`GPU_NOT_FOUND`); the suite passes as
  ten-plus-one. Recorded in AGENTS.

### Top 5 remaining items, ranked

1. **Precipitation and dust wash out against bright backgrounds.** Rain streaks Δ ≤ 3 against
   a 227-DN overcast sky, snow flakes Δ ≤ 5, dust sprites ≤ 4 DN over 14 frames, while each is
   strongly visible against dark ground. The particles carry a fixed radiance; physically a
   flake or streak is lit like any surface — about 0.8× the sky behind it, so it should read
   slightly dark against the deck and bright against the ground. A compositor change in the
   precipitation draw, with the note's deviation and contrast tests on both backgrounds.
2. **Snow has no normal-dependent shading under an overcast sky** (iteration 4's item 1, now
   measured from the air: 0.56 DN across 840 k pixels on saturated terrain). Bevy's ambient is
   flat over normals; an overcast dome is not. Needs a corpus search (overcast sky-luminance
   models) and a research note before any code.
3. **Three GPU tests fail on pristine HEAD, and the clouds suite needs two processes.**
   `cover::gpu_wetness_soaks_and_dries_on_the_formula` (probe 0.992 against < 0.1 after
   drying), `cover::gpu_surface_weathering_follows_the_map`, and
   `voxel::gpu_voxel_snow_covers_the_top` (swap 1 at 22.8–23.1 h against 20.7 ± 2.0, a +2.1 h
   bias with no simulation-side drift since it was written). Plus the adapter cap above.
4. **`day_night` never shows the sun or the moon.** The camera yaw (106.7°) points away from
   the dawn sun (64°) and the sunset sun (303°), and the 23:30 moon is set (−12.3°): the discs
   the domain exists to render cannot appear in the showcase. An `ELEMENTS_HEADING` default that
   tracks the sun at dawn/sunset and a moon-up hour would show them.
5. **The small cosmetic set.** `ELEMENTS_STRIKE_LEAD=6` lands +100 ms after the trigger, past
   the ~43 ms flash decay, so "inside the flash" is wrong in `storm`'s doc; splash rings are
   sparse on a 6 m roof; near-shore foam is sub-pixel at nadir (view-ray thickness); the flood
   marker is outside the capture frustum; fresh erosion cuts facet; the status line clips at
   the window's right edge.

## Iteration 6 — 2026-10-15 — a dust storm streams instead of sitting as static fog

**Commit:** `19db0a4` (code + note + CHANGELOG + the three dust stills). **Sibling repos:** none.

### What changed, and why it ranked first

The full-feature inspection continued (the extra instruction): the iteration-5 backlog's rank 1
was "precipitation and dust wash out against bright backgrounds". Re-measured properly this
iteration — full-band histograms instead of the single 240 × 50 patches the finding came from —
two thirds of it dissolves:

- **Rain reads** (`before6/rain_day.png`, sky band rows 120–260): 50 150 of 161 000 pixels
  (31 %) sit 25–75 DN below the 225 sky — streaks were visible all along; the earlier "Δ ≤ 3"
  was measured on a streak-free patch.
- **Snow reads, sparsely** (`before6/snow_day.png`): flake cores reach 190–204 against a 225
  sky (Δ 21–35) on 4 558 pixels; the earlier "Δ ≤ 5" sampled a faint edge.

The third member was real: **dust sprites were genuinely invisible.** They are drawn at the
plan's 2 mm grain size, which is 0.02–0.1 px anywhere in the 60 m box, so the draw's own
sub-pixel coverage rule (`draw.wesl`, Garg & Nayar 2007 Eqs. 21–22) fades their opacity to
≈ 0.01: 1 247 pixels of 849 920 reached 150–199 against a 224 background and none below 150,
and the reviewer measured ≤ 4 DN of movement over 14 frames — "the storm appears as static
homogeneous fog" (`b5_dust_*`). The example's stated behaviour ("dust sprites stream past the
camera with the wind") did not exist on screen. That is a visible, physically-wrong-at-the-pixel
rank-1 defect in a shipped showcase, and it beat the runner-up (snow's flat overcast shading,
which needs a whole lighting-model note) because it is one constant in one vertex path with a
direct visual oracle.

**The fix:** the draw diameter (`DUST_SPRITE_DRAW_M`, 0.35 m) and the sprite opacity
(`DUST_ALPHA`, 0.5, below a flake's 0.8 so a cluster is a veil rather than a puff) are now
separate from the grain size. `DUST_SIZE_M` (2 mm) remains the size the sprite *extinction*
accounts — Validation 8's 8.7 · 10⁻⁷ m⁻¹ is untouched, as are the counts, the motion, the fog
and every other physics number. `dust.md` Deviation 15 records the split with the measurements.

### Inspection findings

Crops in `/tmp/i6/crops/` (`b12_*`), review in `/tmp/i6/reviews/b12.md` (Gemini Flash,
`screenshot-reviewer`):

- **After the fix the storm reads as particulate:** 26 % of the frame moves more than 6 DN
  (219 924 px), cores reach 150–199 (55 316 px), the tan chromaticity is preserved
  (mean (221, 208, 189) → (216, 203, 183); R:G:B ratio 1.17 : 1.10 : 1.00 before and after),
  and there are no opaque blobs (min channel 181/165/143), no whiteout, no quad edges. The
  reviewer's verdict: *"Does the dust storm now READ as streaming particulate rather than
  static haze? Yes … No artifact introduced … Ship."*
- **Temporal check** (`b12_dust_seqA/B_2x.png`, frames 3 apart): mean difference 4.48 DN,
  clusters advect downwind by (−2…−6, −8…−15) px over 3 frames, edges fade gracefully at the
  box boundary — continuous drift, no popping.
- **Corrections to last iteration's findings** (recorded so the backlog stops carrying them:
  the rain and snow "washout" entries were single-patch sampling artifacts — full-band
  histograms above).

### Verification

- `cargo test`: 211 pass, 0 fail. `cargo clippy --all-targets --all-features -- -D warnings`,
  `RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features`, wasm32 build: clean.
- GPU suites, one process each: `clouds::` 10 + 1 (two parts, the adapter cap), `terrain::` 8,
  `dust::` 6, integration 28 across 11 binaries; `cover::` 5/2 and `voxel::` 0/1 fail exactly
  the three pre-existing tests from iteration 4's backlog item 2 — no new failures.
- `precipitation` unit tests: 16 pass (the sprite extinction pin is unchanged).
- Before/after captures under identical env (`/tmp/i6/before6` vs `/tmp/i6/after6`) and the
  motion sequence (`/tmp/i6/seq6/dust`); the three README dust stills regenerated from the new
  build (600/600/4200 frames, the commands the README documents).

### Top 5 remaining items, ranked

1. **Snow has no normal-dependent shading under an overcast sky** (iteration 5's rank 2):
   saturated terrain reads 0.56 DN of standard deviation across 840 k pixels from the air, and
   the flat-ground case reads < 0.4. Bevy's ambient is flat over normals; an overcast dome is
   not. Needs a corpus search (overcast sky-luminance models) and a research note before code.
2. **Three GPU tests fail on pristine HEAD, and the clouds suite needs two processes**
   (unchanged): `cover::gpu_wetness_soaks_and_dries_on_the_formula`,
   `cover::gpu_surface_weathering_follows_the_map`, `voxel::gpu_voxel_snow_covers_the_top`
   (swap 1 at 22.8–23.1 h against 20.7 ± 2.0), plus the eleven-instance adapter cap.
3. **`day_night` never shows the sun or the moon** (unchanged): camera yaw 106.7° against the
   dawn sun at 64° and the sunset sun at 303°; the 23:30 moon is set at −12.3°. The discs the
   domain exists to render cannot appear in the showcase.
4. **The small cosmetic set** (unchanged): `STRIKE_LEAD=6` outlives the ~43 ms flash decay
   (the doc says "inside the flash"); splash rings sparse on a 6 m roof; near-shore foam
   sub-pixel at nadir; the flood marker sits outside the capture frustum; fresh erosion cuts
   facet; the status line clips at the window's right edge.
5. **Cloud shadow contrast under Clear is unverified** (carried from iteration 5): the crop
   missed the cloud mass, so neither confirmation nor denial. Capture with the shadows in frame
   (choose a heading where cumulus sit upwind) and measure the ground's shadow depressions.

## Iteration 7 — 2026-10-15 — snow relief reads: the overcast dome shades by orientation

**Commit:** `d24c704` (two shaders, the example harness, note, CHANGELOG). **Sibling repos:** none.

### What changed, and why it ranked first

The extra instruction again: inspect every feature with the vision model. The pick is iteration
6's rank 1 (carried from 4 and 5): **snow under an overcast sky is a physically wrong flat
result** — saturated terrain reads 0.56 DN of standard deviation across 840 000 pixels from the
air, because Bevy's ambient is uniform over normals, the beam is ~0 under cloud, and no term in
the stack varies with orientation. It beat the runner-up (`day_night` never framing the sun or
moon) because it is a wrong rendering of a shipped domain in every overcast showcase, while the
runner-up is a showcase's camera choice.

**The fix** (`src/weathered/weathered.wesl`, `src/terrain/render.wesl`, inlined at both call
sites): the lit colour is multiplied by `mix(1, shape(N_y), 1 − T_deck)` where `shape` is the
Moon & Spencer (1942) CIE overcast dome — `L_γ = L_z(1 + 2 sin γ)/3`, the horizon a third of the
zenith (Dáru & Kittler, ESIM 2002, eq. 1; the corpus held no dome model, so the source was
fetched) — integrated over the dome with the ground's bounce at the crate's own albedos into
top 1 / side 0.68 / bottom 0.5, then re-fitted as `1 − 0.35·(1 − N_y)^0.3` [INFERENCE] after the
review's *Adjust*: the pure linear dome is physically honest but gives ≤ 2 DN on 5–15° slopes
(14 pixels ≥ 3 DN on the aerial view), because `cos θ` is flat near vertical; the power curve
keeps the anchors (side 0.65, bottom 0.57) and puts the gradient where the demand is. The ramp
is the raw cloud-map transmittance over each ground point (`cloud_deck`), not
`camera_transmittance`, which is wall-clock-smoothed and varied 0.18–0.77 between identical
runs. Clear air evaluates `T_deck = 1` → factor exactly 1.

The showcase's boxes, columns and spheres were `StandardMaterial` and never reached the shader
(round-1 finding): they now take `WeatheredMaterial` through `ShowcaseProp`, adopted like
`GroundPlane`.

### Implementation findings worth keeping (all cost an hour each)

- **An additive ambient correction clamps to black.** The first form added
  `EnvBRDFApprox × ambient × (shade − 1)` after `apply_pbr_lighting`; sized against Bevy's
  interior ambient it overshoots through the cloud-shadow `ratio` and went negative (forced-
  overcast terrain rendered 0–1 DN). Multiplying the lit colour cannot go negative, and under a
  deck the beam inside it is ~0, so it shapes the fill alone (the beam's share at partial cover
  is the deviation's stated cost).
- **`AmbientLight` is ~0 in this crate** — the additive delta measured ≈0.004 linear where a
  flat ambient would be ~5; the fill arrives with the environment-map light. Any future ambient
  work must scale the lit colour or the env light, not `lights.ambient_color`.
- **A viz that writes a bare `vec4` where `FragmentOutput` is required fails the pipeline**
  ("Entry point fragment … invalid") and the capture then shows the *fog background* — three
  rounds of readings were taken off a failed shader. Viz must set `out.color` and `return out`.
- **The tonemap runs outside the material shader**, so early-returned debug values come back
  ACES+sRGB-encoded; calibrate with constants sentinels before reading them (`0.1/0.4/0.8` →
  `91/149/194`).
- **`ElementsGlobals::camera_transmittance` is smoothed on wall-clock time** — run-dependent
  (0.18–0.77 for identical commands), unusable as a deterministic ramp; the cloud map itself is
  state-derived and deterministic.

### Inspection findings

Reviews in `/tmp/i7/review_b13.md` (round 1, *Adjust*) and `/tmp/i7/review_b14.md` (round 2,
*Ship*), Gemini Flash `screenshot-reviewer`, captures in `/tmp/i7/{before,final,final2}`:

- Round 1: the wash was still flat (99.99 % of the aerial ≤ 1 DN), the showcase props unchanged
  — the reviewer read `examples/common/mod.rs` and found the wiring gap — and the controls
  passed (AE with 2 DN fuzz: 0).
- Round 2: aerial spread 2.35 → 3.28 DN with "clear topological definition and structural
  relief across terrain crests"; overcast ground 629 116 px ≥ 3 DN, 30 196 ≥ 7, peak 9 DN,
  "hillsides and cut banks read unambiguous orientation-dependent shading"; box sides −9 to
  −15 DN with top caps exactly preserved; controls still fuzz-0; no banding, no Mach bands, no
  cloud-map-edge seam.

### Verification

- `cargo test`: 12 suites ok (211 lib tests), 0 fail. `cargo clippy --all-targets
  --all-features -- -D warnings`, `RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features`,
  wasm32: clean (clippy needed a `type` alias for the adoption query).
- GPU suites: `terrain::` 8, `dust::` 6, `clouds::` 10 + 1 (two parts), `tests/gpu_weathered`
  4, integration 11 binaries — all pass; `cover::` 5/2 and `voxel::` 0/1 fail exactly the three
  pre-existing tests from iteration 4's backlog item 2.
- Controls: `tour_clear` AE 0.026 %, `terrain_clear_gnd` AE 0.075 %, both PAE 1 DN and fuzz-2
  AE 0 — clear air renders as before within dither (the props' material fork).

### Top 5 remaining items, ranked

1. **Aerial snow relief is still only 3–6 DN** (round 2 passed it, but it is the weakest
   surface): from 300 m the terrain's slopes are gentle and even the fitted curve gives ≤ 7 DN
   there. The next step is not a steeper curve but occlusion: a sky-visibility/AO term (what
   actually makes overcast relief read in photographs), which needs a research note and the
   corpus first.
2. **Three GPU tests fail on pristine HEAD** (unchanged): `cover::gpu_wetness_soaks_and_dries_
   on_the_formula`, `cover::gpu_surface_weathering_follows_the_map`,
   `voxel::gpu_voxel_snow_covers_the_top` (+2.1 h swap bias), plus the eleven-instance adapter
   cap that splits the clouds suite.
3. **`day_night` never shows the sun or the moon** (unchanged): yaw 106.7° against dawn azimuth
   64° and sunset 303°; the 23:30 moon is set. The discs the domain exists for cannot appear.
4. **The small cosmetic set** (unchanged): `STRIKE_LEAD=6` outlives the ~43 ms flash decay;
   splash rings sparse; near-shore foam sub-pixel at nadir; the flood marker outside the
   capture frustum; erosion-cut faceting; the status line clipping at the window edge.
5. **Cloud shadow contrast under Clear is still unverified** (carried twice): iteration 5's
   crop missed the cloud mass; capture with cumulus upwind and measure the ground's
   depressions.

## Iteration 8 — 2026-10-15 — day_night frames the sun and the moon

**Commit:** `79e5e36` (examples only: `day_night`, `common::FlyCamera`, README, CHANGELOG — no
`src/` change). **Sibling repos:** none.

### What changed, and why it ranked first

The extra instruction again: inspect every feature with the vision model. Two inspection batches
(`/tmp/i8/review_a.md`, `/tmp/i8/review_b.md`, Gemini Flash `screenshot-reviewer`) closed every
rank-1/2 candidate: batch B passed the whole post-dome showcase (sky gradient monotone ≤ 2 DN
per 20 px, horizon continuous with zero shelf, prop top-vs-side shading 17–20 DN, terrain relief
5–8 DN with sub-1-DN speckle, cloud temporal pair at AE 0, rain streaks advection-coherent at
+9 DN against sky and +48 against ground) — **ship-shape**. The open categories therefore fell
to rank 3, and batch A measured the winner with a **Defect** verdict: **`day_night` never shows
the sun or the moon**. Its camera was aimed at bearing 253° (WSW) at +6° pitch while the bodies
sat elsewhere the whole sweep — sunrise 54–65° (ENE, behind), noon 180° at 68° up, sunset 304°
(past the 292° frustum edge), and the waxing moon at 150–175° (south, never in frustum): the
one example whose stated purpose is the celestial discs captured none of them in seven hours.
It beat the runner-up (an occlusion term for the aerial snow relief) because the runner-up
refines a fix the same reviewer already passed this week, while this is a whole missing feature
of the showcase the loop's inspection checklist names explicitly.

**The fix**: an `aim_at_sky` system (`PostUpdate`) turns the camera at the sun while it is
above the horizon, at the moon once the sun is down and the moon is up, and back at the spawned
evening heading when neither is up (`SunState`'s directions; yaw from the same `atan2` the
`camera()` helper uses). While the clock runs the aim follows the bodies; paused
(`ELEMENTS_HOUR`) it aims once (`Local<bool>`), so a still is framed on the disc and the view
stays free to drag — holding the right button releases the aim, which resumes on release. A new
`FlyCamera::face(yaw, pitch)` writes the controller's private fields.

### Verification

- Reviewer round 2 over the same seven hours (`/tmp/i8/review_c.md`, verdict **Ship**): the sun
  disc (10 × 10 px, core 255, centred within 2 px of frame centre) at 04:30 (alt 1.6°), 05:30
  (11.2°), noon (68.3° — the horizon sensibly out of the bottom of frame) and 19:36 (1.3°); the
  crescent moon (4 × 8 px, 37 % illumination faithful, 104–129 DN over the twilight sky) at
  20:12 and 21:30; 23:30 falls back to the evening heading with both bodies set (moon −1.8°) —
  stars-only, near-black ground (7,4,2). Sky checks unchanged from round 1: row steps ≤ 1 DN at
  every pitch, stars 1–2 px, no banding, no status-band occlusion (>300 px clear).
- Gates: `cargo test` 12 suites ok; `cargo clippy --all-targets --all-features -- -D warnings`,
  `RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features`, wasm32 clean. GPU (one process
  per suite): `clouds::` 10 + 1 in the documented two parts (the eleven-instance adapter cap
  fired once when the suite ran whole — `GPU_NOT_FOUND`, not a regression), `dust::` 6,
  `cover::` 5/2, `voxel::` 0/1 (the three pre-existing failures from iteration 4's backlog
  item 2), integration 11 binaries.

### Top 5 remaining items, ranked

1. **Occlusion for the aerial snow relief** (carried): the dome curve gives the aerial view
   3–8 DN; a sky-visibility/AO term is what photographs actually use. Needs a research note and
   a corpus-first search (sky occlusion course notes exist in the corpus: SIGGRAPH 2015 "Sky
   Occlusion", 2006 "horizon shadowing", 2021 "Horizon Occlusion").
2. **Three GPU tests fail on pristine HEAD** (unchanged): `cover::gpu_wetness_soaks_and_dries_
   on_the_formula`, `cover::gpu_surface_weathering_follows_the_map`,
   `voxel::gpu_voxel_snow_covers_the_top` (+2.1 h swap bias); plus the eleven-instance adapter
   cap that requires the clouds suite to run in two parts (in AGENTS).
3. **The small cosmetic set** (unchanged): `STRIKE_LEAD=6` outlives the ~43 ms flash decay
   (the doc says "inside the flash"); splash rings sparse on a 6 m roof; near-shore foam
   sub-pixel at nadir; the flood marker outside the capture frustum; erosion-cut faceting; the
   status line clipping at the window's right edge.
4. **Cloud shadow contrast under Clear is still unverified** (carried twice): iteration 5's
   crop missed the cloud mass; capture with cumulus upwind and measure the ground's shadow
   depressions.
5. **The moon's phase is the only moon feature exercised** (new): the aim fix frames the disc,
   and the reviewer confirmed the 37 % crescent renders faithfully — but no sweep has inspected
   the full moon (a month's phase sweep at a fixed hour) or the moon lighting terrain at
   different phases. Cheap to capture (`WorldClock` date), worth one batch.

## Iteration 9 — 2026-10-16 — cloud shadow and illuminance at a point

**Commits:** `c4f9340` (research note), `13f1092` (implementation, example, test, README), `cd77f34` + `da1a7ce` (0.5.1 release, changelog; the second makes `Environment`'s moon term optional so no new resource is required). **Sibling repos:** none. **Aspect:** clouds (iterations 7 and 8 were ground cover and sky).

**Shipped:** merged to `main` (`1e4c0f3`), tag `v0.5.1`, published at https://crates.io/crates/bevy_elements/0.5.1.

### What changed, and why it ranked first

Gameplay could ask for wind, water, snow and wetness at a point but not for light: the cloud shadow map was GPU-only and `CloudShadows::camera_transmittance` is one number for 70 m around the camera. A stealth system, a shade-seeking creature, a solar panel or a growth model needs the cloud shadow and the light **at a position**. New: `Environment::cloud_shadow_at(Vec3) -> Option<f32>` (direct transmittance toward the light, the position projected along the light as the ground material does, 1 outside the 8 km map, `None` before the first readback) and `Environment::illuminance_at(Vec3) -> f32` (clear-sky sun + moon × Kasten & Czeplak's `0.25 + 0.75·T`). The `cloud_shadow` kernel stores every fourth texel's `T` in a 256² buffer (31.25 m cells) inside the existing dispatch; the 4 Hz readback copies it with the origin and light direction captured at request time. Cost: `elements_clouds_shadow` 0.083 → 0.087 ms (min, High, 3840×2160, P2 1590–1725 MHz). New example `cloud_shadow_probes`: a 15×15 grid of unlit markers coloured by the query (yellow open, blue shadowed) with the centre's lux on screen and logged every 120 frames.

### Candidates scored (1–5)

| Candidate | Game hook | Visible | Correctness | Cheap | Sum | Ownership |
|---|---|---|---|---|---|---|
| **Cloud shadow + illuminance at a point** | 5 | 3 | 3 | 5 | **16** | ours |
| Occlusion term for the aerial snow relief (backlog 1) | 1 | 3 | 3 | 3 | 10 | ours |
| The three pre-existing failing GPU tests (backlog 2) | 2 | 1 | 4 | 3 | 10 | ours |
| Cloud shadow contrast under Clear verified (backlog 4) | 1 | 3 | 3 | 5 | 12 | ours (folded into this iteration's inventory) |
| Full-moon / moonlit terrain sweep (backlog 5) | 1 | 3 | 2 | 5 | 11 | ours |
| Save/restore of the weather state | 4 | 1 | 2 | 3 | 10 | ours |

### Shot list (Gemini Flash `screenshot-reviewer`, `/tmp/i9/r1.md`–`r7.md`)

| File | Inventory element | Verdict |
|---|---|---|
| `/tmp/i9/a_pc_above.png` (PartlyCloudy 11:00, 120 m, −25°) | (a) ground shadows, (b) probes | Defect (major): no shadow crossed the 560 m grid (0/225) — test-design defect, fixed by height-scaled spacing and a heading scan |
| `/tmp/i9/b_pc_grazing.png` (6 m, −3°) | (a), (b) | Defect: same cause (0/225) |
| `/tmp/i9/c_clear_above.png` (Clear) | (a), (b) | Pass |
| `/tmp/i9/d_pc_afternoon_high.png` (17:00, 400 m, −45°) | (a), (b) | Defect: grid wholly in one shadow (225/225, probes all blue, consistent) — no edge in view |
| `/tmp/i9/{a,d}_*_crop{2x,4x}.png` | (c) edges | Pass ×4 (no edge in crop) |
| `/tmp/i9/e_overcast.png` | (a), (b) under a deck | Pass: 225/225 blue, ground uniform |
| `/tmp/i9/seq_sheet.png` (6 frames, 1.5 s apart) | (b) temporal stability | Pass (r6): probes stable, lux readout smooth; r3's "blocker" was the 0/225 grid of round 1 |
| `/tmp/i9/c_clear_above_crop2x.png` | (c) | Pass |
| `/tmp/i9/f_pc_1500.png` (1500 m, −55°, 250 m spacing) | (a), (b) | Defect (major) → resolved by crops: the patch is faint at 2–3 km (haze); crops show the probe colour change on it |
| `/tmp/i9/g_pc_800.png` (800 m, −35°, 150 m) | (a), (b) | Defect (major) → resolved by crops: in-patch probes are partial blends (thin cloud), readout threshold 0.5 |
| `/tmp/i9/h_pc_1500_16h.png` (16:00) | (a), (b) | Pass: 220/225 blue, every visible probe blue |
| `/tmp/i9/i_pc_200_grazing.png` (40 m, −4°) | (a) bands with depth | Defect (major): no bands visible — see open item 1 |
| `/tmp/i9/{f,g}_*_crop{2x,4x}.png` | (c) | Pass ×4: probe colour change on the ground's gradient within one spacing; soft edges, no texel stepping |
| `/tmp/i9/j_pc_h300.png` (120 m, −25°, heading 270, 36/225) | (a), (b), (c) | **Pass**: patch 14 % darker, ~32 blue probes all inside it, grey probes on the edge |
| `/tmp/i9/k_pc_h315.png` (heading 315, 33/225) | (a), (b), (c) | **Pass**: diagonal soft edge, grey probes along the 150–158 DN contour |
| `/tmp/i9/l_pc_h315_grazing.png` (8 m, −4°) | (a) | Defect (major): no bands — open item 1 |
| `/tmp/i9/n_pc_grazing30.png` (30 m, −6°, 50/225) | (a), (b) | (b) Pass: blue probes form a coherent receding band; (a) Defect: ground only 4–5 % darker — open item 1 |
| `/tmp/i9/m_tour_pair.png` (weather_tour PartlyCloudy, base `b7529f9` vs this) | before/after | **Pass**: 0 px differ at 2 % fuzz; ground and sky identical |

### Defects found, fixed, re-shot

- Round 1 grid never met an edge (40 m spacing, 560 m): the example's spacing now scales with height (`ELEMENTS_SPACING`, default height/3 ≥ 40 m), the readout is logged, and a heading scan (0–315°, 120 m) found shadowed probes at 270° and 315° — re-shot as j/k, both **Pass** with edges.
- Grazing (a) investigated rather than assumed: a throwaway build tinted `t < 0.5` blue in `weathered.wesl` (reverted, rebuilt). The ground's `t < 0.5` pixels lie exactly where the blue probes are (rows 288–336 of n), and there the debug tint's red channel still reads ~95 DN: the aerial perspective adds ~95 DN at 0.3–1 km, so a ¼-light shadow reads 8 % darker. Query and render agree; the contrast loss is haze.

### Defects still open

1. **Grazing cloud shadows are washed out by the haze** (minor; not this change): from 6–40 m the nearest PartlyCloudy shadows over the origin are 0.3–1 km out, where inscatter adds ~95 DN; they read 4–8 % darker. The note's grazing target (a) is unmet; an example with a shadow nearer the camera, or a check that the haze level at 1 km under `PartlyCloudy` is right, is the follow-up.

### Verification

- New tests: `query::tests` (2, headless: `None` before a readback and the cover estimate for the light; a shadowed cell, its neighbour, a raised point projected along the light, outside the map, illuminance ratio 0.25) and `gpu_clouds::the_cloud_shadow_query_reads_the_shadow_map` (2 704 samples, mean |query − map| 0.0000, 1 314 lit / 1 247 dark, 27 324–109 298 lx).
- Gates: `cargo build --release`, `cargo test` (all suites ok), `cargo build --examples --all-features`, clippy `-D warnings`, `RUSTDOCFLAGS=-D warnings cargo doc`, wasm32 — clean. GPU: integration binaries all ok (gpu_clouds 5, hydrology 9, imports 1, precipitation 6, water 2, weathered 4, wind 2); lib `terrain::`, `dust::` ok; `clouds::` 10 + `the_horizon_sky` 1 ok; `cover::` 5/2 and `voxel::` 0/1 — the three documented pre-existing failures.

### Top 5 remaining items, ranked

1. **Grazing cloud shadows vs haze** (new, open item 1): verify the `PartlyCloudy` aerial perspective at 1 km against the visibility the weather states (95 DN of inscatter at 1 km is a lot for 20+ km visibility); if it is right, frame a near shadow in an example.
2. **Three GPU tests fail on pristine HEAD** (unchanged): `cover::gpu_wetness_soaks_and_dries_on_the_formula`, `cover::gpu_surface_weathering_follows_the_map`, `voxel::gpu_voxel_snow_covers_the_top`.
3. **Occlusion for the aerial snow relief** (carried).
4. **The moon's phase sweep / moonlit terrain** (carried); `illuminance_at` now gives a number to check the moonlight against (full moon ≈ 0.1–0.3 lx).
5. **The small cosmetic set** (carried): `STRIKE_LEAD`, sparse roof splashes, nadir foam, flood marker, erosion faceting, status-line clipping.

## Iteration 10 — 2026-10-16 — gameplay queries stay fresh without a window

**Commits:** `3ad816d` (research note `docs/research/readback-pacing.md`), implementation + test + docs, 0.5.2 release, this log. **Sibling repos:** none.

**Shipped:** merged to `main` (`b6a690a`), tag `v0.5.2`, published at https://crates.io/crates/bevy_elements/0.5.2. **Aspect:** gameplay queries / GPU readback (iterations 8 and 9 were sky and clouds).

### What changed, and why it ranked first

Backlog item 2, the three GPU tests failing on pristine HEAD since iteration 4, turned out to be a consumer-visible bug, not test noise. Traced (temporary prints in `readback.rs`, reverted): in an app with no window every readback's request-to-delivery latency went 2, 4, 8 … frames and the last delivery of any field was 3.1 s into the run; 85 s later `Environment::wetness` answered 0.325 while the map read 0.0. Only presenting a swap chain makes the CPU wait for the GPU, so a headless app (a server, a simulation tool, every GPU test) queued frames without bound and every gameplay query froze on the state of its first seconds. A non-blocking `Poll` per frame only moved the freeze to 23 s: the queue, not the callbacks, was the cause. Windowed apps were fine (`cloud_shadow_probes` logged `snapshot_age` 250 ms, one interval, over 1500 frames).

**The fix**: `pace_without_window` (`src/gpu/readback.rs`, `Render`/`PostCleanup`, after `map_buffers`): a frame with no `ExtractedWindow` waits for its own GPU work (`RenderDevice::poll(PollType::wait_indefinitely())`), so the queue is bounded at one frame and each readback lands on the next frame. With a window it returns at once. No-op on WebGPU.

It ranked first over the haze item (backlog 1): measuring the haze showed the distance fog's colour already matches Bevy's horizon (fog ≈ 0.53 linear display vs horizon sky 0.6–0.7) and the 30 km `PartlyCloudy` extinction gives 12 % fog at 1 km, so the iteration-9 "95 DN of inscatter" was the auto exposure lifting sunlit ground beyond a cloud shadow, not a fog defect — no change there (left in the backlog as a capture item, below).

### Candidates scored (1–5)

| Candidate | Game hook | Visible | Correctness | Cheap | Sum | Ownership |
|---|---|---|---|---|---|---|
| **Readbacks freeze without a window (backlog 2's root cause)** | 5 | 1 | 5 | 5 | **16** | ours |
| Grazing cloud shadows vs haze (backlog 1) | 1 | 3 | 2 | 3 | 9 | ours (measured: no fog defect) |
| Occlusion for the aerial snow relief (backlog 3) | 1 | 3 | 3 | 3 | 10 | ours |
| Moon phase sweep / moonlit terrain (backlog 4) | 1 | 3 | 2 | 5 | 11 | ours (sky: last two iterations) |
| Mesh distance fog double-counts the 40 km clear-air floor that Bevy's aerial perspective also draws | 1 | 2 | 3 | 5 | 11 | ours (Bevy's ground extinction is ~2×10⁻⁵ m⁻¹ against the 9.8×10⁻⁵ floor: ≤ 2 % at 1 km) |
| Save/restore of the weather state | 4 | 1 | 2 | 3 | 10 | ours |

### Inventory and shot list (Gemini Flash `screenshot-reviewer`, `/tmp/i10/review1.md`)

The change creates and alters no visual element: the wait never runs in a windowed app. The inventory is therefore the windowed frame itself, checked before/after.

| File | Inventory element | Verdict |
|---|---|---|
| `/tmp/i10/pair_PartlyCloudy.png` (`base_tour_PartlyCloudy.png` vs `after_tour_PartlyCloudy.png`, `weather_tour` noon) | windowed frame unchanged | **Pass**: 1 px of 921 600 differs by 1 LSB (GPU noise); sky, clouds, horizon, ground, props identical |
| `/tmp/i10/pair_Overcast.png` (`base_tour_Overcast.png` vs `after_tour_Overcast.png`) | windowed frame unchanged | **Pass**: bit-identical (same SHA-256); no banding, shelf or black holes in either half |

### Defects found, fixed, re-shot

None visual. The functional defect is the change itself.

### Defects still open

None from this change.

### Verification

- New test `tests/gpu_readback.rs::the_queries_stay_fresh_without_a_window` (`Ultra`, 256² terrain, 100 ms interval, 1200 frames): oldest snapshot **683 ms before** the fix (fails the 200 ms limit), **83 ms after**.
- Gates: `cargo build --release`, `cargo test` (all suites ok), `cargo build --examples --all-features`, clippy `-D warnings`, `RUSTDOCFLAGS=-D warnings cargo doc`, wasm32 — clean. GPU, one process per suite: integration binaries all ok (gpu_clouds 5, hydrology 9, imports 1, precipitation 6, readback 1, water 2, weathered 4, wind 2); lib `terrain::` 8, `cover::` **7/7**, `dust::` 6, `voxel::` **1/1**, `clouds::` 10 + `the_horizon_sky` 1. **The whole GPU suite is green for the first time since iteration 4.**
- Note for the loop: `.omp/skills/bevy-elements-build-loop/SKILL.md` §6.5 still lists the three tests as pre-existing failures; they now pass and any failure is a regression (the skill is user-authored, so it is not edited here).

### Top 5 remaining items, ranked

1. **Occlusion for the aerial snow relief** (carried).
2. **The moon's phase sweep / moonlit terrain** (carried); `illuminance_at` gives the number to check (full moon ≈ 0.1–0.3 lx).
3. **Save/restore of the weather state** (new to the top 5): a game hook for save games — `Weather`, `WorldClock`, the director's schedule and the cover/water layers.
4. **Grazing cloud-shadow capture** (re-scoped from iteration 9's item 1): the fog is right; frame a shadow within 300 m of a grazing camera so the shadow contrast can be judged without 1 km of air.
5. **The small cosmetic set** (carried): `STRIKE_LEAD`, sparse roof splashes, nadir foam, flood marker, erosion faceting, status-line clipping.

## Iteration 11 — 2026-10-16 — the weather can be saved and restored

**Commits:** `7a0a241` (research note `docs/research/weather-save.md`), implementation + test, 0.5.3 release, this log. **Sibling repos:** none.

**Shipped:** merged to `main` (`8f90456`), tag `v0.5.3`, published at https://crates.io/crates/bevy_elements/0.5.3. **Aspect:** climate and weather / save state (iterations 9 and 10 were clouds and GPU readback).

### What changed, and why it ranked first

Backlog item 3. A game could serialize `WorldClock`, `Climate`, `GeoLocation` and `Weather`, but not `WeatherDirector`: private fields, no `serde`, no `Reflect`. A reloaded save therefore started a fresh director, which arms at `Clear` on its first frame, drops a forced preset, cuts a blend in progress, and then walks a different Markov chain (each hour's step depends on the previous preset). `WeatherDirector` and its private `Blend`, `Transition` and `Request` now derive `Reflect`, `PartialEq` and (feature `serde`) `Serialize`/`Deserialize`. The scheduler's draws are a pure hash of `(seed, hour)`, so the chain position is the only state; no RNG cursor exists to save. The highest Game hook of the open items, a small CPU-only surface, no GPU cost. The GPU layers (snow cover, wetness, water, terrain) are out of scope and listed below.

### Candidates scored (1–5)

| Candidate | Game hook | Visible | Correctness | Cheap | Sum | Ownership |
|---|---|---|---|---|---|---|
| **Save/restore of the weather director (backlog 3)** | 5 | 1 | 4 | 5 | **15** | ours |
| Save/restore of the GPU layers (cover, water, terrain) | 5 | 1 | 3 | 1 | 10 | ours (needs whole-layer readback and upload) |
| Occlusion for the aerial snow relief (backlog 1) | 1 | 3 | 3 | 3 | 10 | ours |
| Moon phase sweep / moonlit terrain (backlog 2) | 1 | 3 | 2 | 5 | 11 | ours |
| Grazing cloud-shadow capture (backlog 4) | 1 | 3 | 1 | 5 | 10 | ours (capture only) |

### Inventory and shot list (Gemini Flash `screenshot-reviewer`, `/tmp/i11/review1.md`)

The change creates and alters no visual element (CPU derives only). The inventory is the windowed frame, before (`main`, `/tmp/base`) and after.

| File | Inventory element | Verdict |
|---|---|---|
| `/tmp/i11/pair_PartlyCloudy.png` (`base_`/`after_PartlyCloudy.png`, `weather_tour` noon) | windowed frame unchanged | **Pass**: 0 px differ at 2 % fuzz, max 2/255 |
| `/tmp/i11/pair_Thunderstorm.png` (`base_`/`after_Thunderstorm.png`) | windowed frame unchanged | **Pass**: 0.3 % px differ, all thin rain streaks (a 2×2 opening removes every one); mean signed delta ≈ 0, no sky/cloud/ground change |

### Defects found, fixed, re-shot

None.

### Defects still open

None from this change.

### Verification

- New `tests/save.rs` (`required-features = ["serde"]`, `serde_json` dev-dependency): a director saved through JSON after 72 game hours and loaded into a fresh app gives the same `Weather` as the uninterrupted run at all 720 updates of the next 72 h (with preset changes in that window); restoring only the clock diverges; a director saved one hour into a two-hour forced Thunderstorm blend restores forced, at the same blend, and reaches 25 mm/h. Before the fix the test does not compile (`WeatherDirector: serde::Serialize` not satisfied).
- Gates: `cargo build --release`, `cargo test` (all suites ok), `cargo test --features serde --test save` (2 ok), `cargo build --examples --all-features`, clippy `-D warnings`, `RUSTDOCFLAGS=-D warnings cargo doc`, wasm32 — clean. GPU, one process per suite: gpu_clouds 5, hydrology 9, imports 1, precipitation 6, readback 1, water 2, weathered 4, wind 2; lib `terrain::` 8, `cover::` 7, `dust::` 6, `voxel::` 1, `clouds::` 10 + `the_horizon_sky` 1 — all ok.
- `hs status` failed in the loop's shell ("No such device or address", no TTY); research used the synced corpus (Gregory 2018 §16.4.5) and the existing climate-weather sources.

### Top 5 remaining items, ranked

1. **Save/restore of the GPU layers** (new): snow cover, wetness, water and terrain live in storage buffers; a save needs a whole-layer readback and a restore an upload through `Assets<ShaderBuffer>`.
2. **Occlusion for the aerial snow relief** (carried).
3. **The moon's phase sweep / moonlit terrain** (carried); check against `illuminance_at` (full moon ≈ 0.1–0.3 lx).
4. **Grazing cloud-shadow capture** (carried): frame a shadow within 300 m of a grazing camera.
5. **The small cosmetic set** (carried): `STRIKE_LEAD`, sparse roof splashes, nadir foam, flood marker, erosion faceting, status-line clipping.

## Blocked pass — 2026-10-17 — the Opus 5.5 reviewer cannot run

No iteration was started. The skill's preflight sent one image to the `screenshot-reviewer` agent (`openrouter/anthropic/claude-opus-5.5`) before any capture, and it failed in 0.6 s:

> 402 This request requires more credits, or fewer max_tokens. You requested up to 65536 tokens, but can only afford 42467. To increase, visit https://openrouter.ai/workspaces/default/keys/bd57b3406fa7580c26d459649a68144eccacb275026701a7bfaf041bef55a088 and adjust the key's monthly limit

Every iteration needs a GREAT verdict from this reviewer, so the pass stopped (skill Rules: a reviewer that cannot run is a blocker). Unblock by raising the OpenRouter key's monthly limit. 42 467 affordable output tokens at $20 per 1M is about $0.85 left, under one multi-image review.

State at the stop: `main` at `096719b`, every gate and GPU suite green (2026-10-17). home-still healthy (distill idle on CUDA, both scribes up). The backlog is unchanged from iteration 11, plus the crate-review fixes F1–F11 that landed between iterations (`12ad5f3`..`4cf0527`). F12 (bind groups re-created every frame) was not fixed and is not measured.

Second pass, same day: the reviewer preflight failed again in 0.4 s, `402 … You requested up to 65536 tokens, but can only afford 42177.` The key's limit has not been raised; the pass stopped before any capture.

Third pass, same day: preflight failed again, `402 … can only afford 42174.` Stopped before any capture.

## Iteration 12 — 2026-10-17 — twilight, the sunset exposure and the night sky

**Commits:** `5a96d37` (research note `docs/research/twilight.md`), `2559c3f` (implementation + `tests/gpu_twilight.rs`), `80f3772` (README, CHANGELOG, `celestial.md` Deviation 19, regenerated stills), this log. **Sibling repos:** none. **Not released** (the loop no longer bumps, tags or publishes). **Visual judgment:** by the loop agent itself (user decision 2026-10-17, after the Opus 5.5 reviewer hit OpenRouter's credit limit three passes in a row).

### What changed, and why it ranked first

The baseline sweep (29 captures, `/tmp/i12/before/`, findings in `/tmp/i12/critique.md`) found three linked defects in the sky, each measured on a linear `Rgba16Float` target:

1. **Every night sky was brown, ~40 DN.** Bevy's default `ClearColor` (sRGB 43, 44, 47) is written behind the sky before any exposure and Bevy's atmosphere draws over it with its transmittance: a constant linear (0.022, 0.0205, 0.0186) behind every sky pixel with every light off, on four dates.
2. **Twilight did not exist.** The sun's light was switched off at −2° (an unsourced M1 constant), so from −2° to −18° Bevy's atmosphere had no light; what showed was item 1.
3. **The low sun blew the frame out.** The illuminance-metered exposure left the sky toward the sun at 6–20× display peak from +12° to −2° (`dn_dusk.png` zenith 245 DN; `wt_sun_18.6` 252 DN).

Fixed: the sun lights the atmosphere to −18°, scaled below the horizon by `g(h)` fitted on Bevy's renderer to Hulburt's measured zenith twilight (`k` 0.44/° to −6°, then Hulburt's own 0.983/°); below the horizon its cloud factor is the dome's total so the composite's `sky_gain` is 1; the precipitation draw takes the sun only while its disc is up; the auto exposure is the larger of the illuminance target and a cap from the sky toward the sun (Patry 2021's highlight threshold, ceiling 3.2× display peak, the glow at a 27° sun); a camera that kept Bevy's default clear colour is cleared to the night sky's own glow (`E_floor/π` = 1.6×10⁻⁴ cd/m²) at its exposure.

It ranked first because the twilight sky had no light source at all, wrong for an hour of every evening in any game with a day cycle, and its fix also needed the exposure and the background. Runner-up: the flat, washed-out terrain shading on `hills` and `mountains`, a matter of degree rather than a missing light.

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **Twilight missing, brown night sky, low-sun blowout** | 1 (wrong sky for the time) | picked |
| 2 | Flat terrain shading (`tt_hills42_eye`, `tt_mtn_elev`) | 2 (beauty) | runner-up |
| 3 | Night thunderstorm rain reads as white static (`storm.png`) | 2 | needs a look at streak brightness under a flash |
| 4 | Clouds in twilight flat grey; moonlit cloud rims | 2 | cloud lighting below `SUN_HANDOVER_DEG` |
| 5 | Pillar edge steps through the cloud composite (`dn_dusk` 4× crop) | 1 (cosmetic) | composite against geometry |
| — | Backlog: GPU-layer save, aerial snow occlusion, moon phase sweep, grazing shadow capture, cosmetic set | | carried |

### Sources

Hulburt 1953, *JOSA* 43:113 (`10.1364/JOSA.43.000113`); Haber, Magnor & Seidel 2005, *ACM TOG* 24(4) (`10.1145/1095878.1095884`) — both fetched through SciDB and read from `pdftotext`, not yet ingested (S3 upload failure, note's `Pending index`); Bruneton & Neyret 2008 (`10.1111_j.1467-8659.2008.01245.x`); Hillaire 2020 (`10.1111_cgf.14050`); Patry 2021 (`s2021-advances-pdf-66-mb`); Jensen et al. 2001 (`10.1145_383259.383306`, through `hs distill search`); Bevy 0.20 source (`pbr_lighting.wesl` visible-sun factor, `volumetric_fog.wesl`, `ScatteringMedium::earth`'s ozone).

### Critique and verdicts (`/tmp/i12/critique.md`; before `/tmp/i12/before/`, after `/tmp/i12/after/`)

| Feature | Evidence | Verdict |
|---|---|---|
| Twilight sky, toward the sun (−2.2° … −10.9°) | `after/tw/wt_sun_19.{0,3,6,9}.png`, `cmp/sheet_wt_sun_19.3.png` | GREAT: violet sky over an orange band shrinking to a red afterglow under navy with stars |
| Twilight sky, away from the sun | `after/tw/wt_away_19.{0,3,6}.png` | GREAT: Earth's shadow under the pink Belt of Venus |
| Golden hour / sunrise exposure | `after/tw/wt_sun_18.6.png`, `wt_away_18.6.png`, `dn_dawn.png` | GREAT: no white field, orange cloud rims, silhouettes |
| Zenith (GPU) | `tests/gpu_twilight.rs` | 0.94×, 1.01×, 0.65× of Hulburt at −2.2°, −3.9°, −5.5°; blue-purple |
| Night background | `after/tw/wt_night_23.png`, `wt_*_21.2.png` | GREAT; cosmetic: the horizon reads (9,5,2) DN, the neutral glow reddened by the horizon transmittance, visible only stretched |
| Continuity | `/tmp/i12/lapse_after.png`, `lapse_after.gif` (19:24 → 22:36) | GREAT on round 3 (round 1: olive bloom then a snap to black from Bevy's multiple-scattering floor, fixed by the −6° tail; round 2: the same from `sky_gain` amplifying the night 250×, fixed by the dome-total cloud factor) |
| Clouds in twilight / moonlit | `after/tw/wt_sun_19.3.png`, `wt_sun_21.2.png` | not touched; NOT YET, next item |

Daytime regression check (MAE against before, same frames): every noon and terrain capture ≤ 0.002 except `wt_PartlyCloudy_noon` 0.086 and `wt_Fog_noon` 0.059 — deterministic (after-vs-after 0): a camera in a cloud shadow no longer opens up past the sunlit sky (0.4 stop darker, richer sky), kept and documented. `docs/storm.png` was not regenerated: its change is the flash moment (nondeterministic), not this iteration.

### Verification

- New tests: `tests/gpu_twilight.rs` (3 GPU tests; all three fail on `eaf51e0`: glow 21.9× peak, night floor 0.022, zenith ratio 0.066 against Hulburt's 0.18), `celestial` unit tests for the twilight factor, the disc factor, the glow cap's range and the sky gain once the sun has set.
- Gates: `cargo build --release`, `cargo test` (all ok, lib 221; `the_lightning_schedule_is_frozen` unchanged), flora/voxel feature tests, `cargo build --examples --all-features`, clippy `-D warnings`, `RUSTDOCFLAGS=-D warnings cargo doc`, wasm32 — clean. GPU, one process per file/module: clouds 5, hydrology 9, imports 1, plugins 1, precipitation 6, readback 1, twilight 3, water 2, weathered 4, wind 2; lib `clouds` 10 + `the_horizon_sky` 1, `terrain` 8, `cover` 7, `dust` 6 — all ok.
- Performance: no GPU pass added; the sun's shadow cascades are now off below −0.5° (they were on to −2°). `bench_elements` at the default 4K/High and at 1080p could not allocate: another tenant (`VLLM::EngineCore` 15.9 GB + `hs-distill-server` 7.5 GB) held 23.5 of the card's 24 GB. Measured at `ELEMENTS_QUALITY=low ELEMENTS_TERRAIN=none ELEMENTS_PRESET=PartlyCloudy`, 1920×1080, GPU P2 at 1755–1800 MHz logged in the same shell: total min 0.571 ms before, 0.558 ms after; every pass's minimum within 0.006 ms.
- `lake` example: out of GPU memory in the same shared-memory state (environment, not a defect).

### Top 5 remaining items, ranked

1. **Clouds in twilight and moonlight**: post-sunset cumulus are flat neutral grey (no warm underside for the first degrees, no sky tint), and moonlit cloud edges form a light rim around a dark core against the now-dark sky (`pick_light` hands over at −4°; `cloud_ambient` tint).
2. **A view-dependent exposure cap**: the glow cap ignores where the camera looks, so a camera facing away from a low sun is up to ~2.5 stops darker than the illuminance alone (`docs/dawn.png`); meter the brightest sky in the frame (Patry's screen highlights).
3. **Flat terrain shading** (runner-up): `hills` reads uniform green at noon, `mountains` pale at 16 h.
4. **Night thunderstorm rain** (`storm.png`): flash-lit streaks read as white static over a black sky.
5. **Save/restore of the GPU layers** (carried) and the cosmetic set (`STRIKE_LEAD`, sparse roof splashes, nadir foam, flood marker, erosion faceting, status-line clipping, the composite's pillar edge step).

## Iteration 13 — 2026-10-17 — clouds take the colour of the light that fills them

**Commits:** `9ec2c62` (research note, `clouds.md` Deviation 30), `8de42a5` (implementation + `tests/gpu_cloud_ambient.rs`; the note's Deviation 30 and Validation 22 rewritten to the shipped model), `d3af2cf` (README, CHANGELOG, regenerated stills), this log. Merged through a pull request. **Sibling repos:** none. **Not released.**

### What changed, and why it ranked first

The clouds' ambient (the diffuse light on their shaded sides) was `vec3(ambient)`, a neutral grey scalar whatever the sky, so after sunset every cloud was grey cardboard against a violet or navy sky (civil twilight base (57, 57, 59) under a violet sky; nautical twilight (76, 77, 77) against navy (49, 52, 68)). Backlog item 1 of iteration 12. The ambient now takes the chromaticity of the diffuse light it stands for: the clear sky's irradiance from the camera's atmosphere environment map (16 Fibonacci directions over the upper hemisphere, divided by the cloud factor the sun's light carries so the sky above the layer is undimmed, plus the night-sky background) plus the share of the light's beam the cloud above the sample has scattered, $E_\odot \hat l_y (1 - e^{-\tau_l})$. Its luminance is unchanged.

Round 1 (sky colour alone) gave royal-blue night clouds in a brown-black sky: the cube held a moonlit atmosphere negligible next to the airglow floor; adding the floor fixed it. Round 2 passed the twilight frames but the GPU veil test failed (far-row tint 0.249 vs 0.126) and the horizon capture showed why: the noon `Overcast` deck turned periwinkle above a white band, because deep in a thick deck only the ambient is left. Round 3 adds the scattered sun, the physical reason behind Hillaire's "slightly blue" clouds and his artists' desaturation knob, which the model therefore does not need.

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **Grey cloud ambient against coloured twilight skies** | 1 (wrong colour for the light) | picked |
| 2 | View-independent exposure cap (anti-solar sky darker than its clouds at −8°) | 2 | runner-up, owns the nautical contrast |
| 3 | `tt_mtn_pc_aerial` 2-px dark line at y ≈ 298 | 1 (cosmetic) | |
| 4 | Overcast at −4° a featureless flat deck | 2 | |
| 5 | Rain at noon: a uniform near-sky wall | 2 | |

### Sources

Hillaire 2016 §5.5.1 (ambient from Frostbite's sky probe; desaturation because clouds came out slightly blue; single-colour ambient listed as the first thing to improve), Schneider & Vos 2015 (`s2015-advances-ppt-944-mb-pdf-5-mb-2`, ll. 1218–1240: ambient from the sky by time of day), Bevy 0.20 `atmosphere/environment.wesl` (the cube is `sample_sky_view_lut`, absolute radiance before exposure).

### Critique and verdicts (`/tmp/i13/critique.md`; before `/tmp/i13/before/`, after `/tmp/i13/after/`, horizon `/tmp/i13/hz/`)

| Feature | Evidence | Verdict |
|---|---|---|
| Civil twilight (−4.2°), PartlyCloudy and Overcast | `wt_*_sun_19.2`, `wt_Overcast_away_19.2` | GREAT: violet-grey base (62, 55, 62), rose-grey deck (136, 126, 135) |
| Nautical twilight (−8.1°) away from the sun | `wt_PartlyCloudy_away_19.6` | GREAT: mauve (81, 74, 83) from the glow that lights it; its brightness against the navy sky is the runner-up's |
| Golden hour (+1.8°) | `wt_PartlyCloudy_sun_18.6` | GREAT: warm taupe body (74, 59, 56) under the orange rim |
| Noon PartlyCloudy, Overcast, Snow, horizon | `r3_day.png`, `hz/sheet3.png` | unchanged neutral decks (round 2's blue overcast fixed) |
| Night (crescent moon) | `wt_*_21.2` | the night sky's hue (14, 11, 9) |
| Continuity | dusk lapse 19:00 → 21:48 (2026-06-15), 20 consecutive frames at 20:30 | monotone through the sun→moon handover, ≤ 1 DN a frame |

### Verification

- New test `tests/gpu_cloud_ambient.rs::the_deck_takes_the_colour_of_its_light`: dusk red/green 1.26 vs 1.02 neutral; noon blue/red 1.05× neutral, bounded to (1.0, 1.1) (the sky-colour-only build gave 1.27×); fails on `162c33b` (identical decks with and without the map). Validation 20 (veil) holds: far-row tint differences ≤ 0.036.
- Gates: `cargo build --release`, `cargo test` (0 failures), `cargo build --examples --all-features`, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32 — clean. GPU, one process per file/module: every `tests/gpu_*.rs` file and lib `clouds` (10), `terrain` (8), `cover` (7), `dust` (6), `voxel` (1), `the_horizon_sky` — all pass.
- Performance (`bench_elements`, `ELEMENTS_QUALITY=low ELEMENTS_TERRAIN=none ELEMENTS_PRESET=PartlyCloudy`, 1920×1080, P2 at 1740–1860 MHz logged in the same shell, two alternating rounds): `elements_clouds_march` min 0.190 / 0.184 ms before, 0.184 / 0.189 ms after; total 0.598 / 0.587 against 0.573 / 0.590 ms. No measurable cost. (The shared card still holds ~22 GB for other tenants; High/4K benches do not fit.)
- Stills: `docs/cloud_sunset.png`, `twilight.png`, `belt_of_venus.png`, `sunset.png` regenerated at their recovered poses (the base build reproduces the old stills to MAE ≤ 0.0008); `tour.gif` (noon) unchanged by this iteration.

### Next feature (user direction, 2026-10-17)

Asked to review the crate for the single feature most useful to game developers: **interactive ground trails** — feet, wheels and bodies pressing tracks into snow, sand and mud that new snowfall, wind and time erase. Every interaction the crate offers runs one way (environment → game: `Environment`, `WeatheredMaterial`, `PrecipitationBlocker` shelter); nothing a character does marks the world, and the terrain's 4 m cells cannot hold a footprint. Literature pulled for it: Sumner, O'Brien & Hodgins 1999 (arXiv 2302.08683), Alvarado et al. 2022 (`10.3389/frvir.2022.801856`), Su et al. 2023 (`10.1145/3610548.3618159`), Goswami 2024 survey (`10.1111/cgf.15059`), Hanák 2021 (MUNI thesis, surveys AC III, Batman, Rise of the Tomb Raider, HZD Frozen Wilds, TLOU II), Svensson 2019 (Umeå thesis), Barré-Brisebois 2014 (GDC), CRREL Report 90-9 (shallow-snow sinkage), Wong's Bekker table via NASA TM-20250006958, Tatarchuk 2006 POM (`s2006-advances-materials-10`).

### Top 5 remaining items, ranked

1. **Interactive ground trails** (the next iteration, above).
2. **A view-dependent exposure cap** (carried): meter the brightest sky in the frame.
3. **Flat terrain shading** (carried).
4. **Night thunderstorm rain** reads as white static (carried).
5. **Save/restore of the GPU layers** and the cosmetic set (the aerial's 2-px line at y ≈ 298, the composite's pillar edge step, `STRIKE_LEAD`, sparse roof splashes).

## Iteration 14 — 2026-10-17 — trails: footprints and ruts in snow, sand and mud

**Commits:** `9033ff1` (research note `docs/research/trails.md`), the implementation with `tests/gpu_trails.rs` (the note's Deviations 1–10 and measured validations), the README / CHANGELOG / stills / GIF, this log. Merged through a pull request. **Sibling repos:** none (`bevy_flora` owns no ground marks). **Not released.**

### What changed, and why it ranked first

Every interaction the crate offered ran from the environment to the game; nothing a character did marked the world, and the terrain's 4 m cells cannot hold a footprint. `TrailsPlugin` adds the missing direction: an `Imprint` presses a camera-following window of centimetre texels (High 1024² × 5 cm) to its Bekker–Wong sinkage (Wong's constants; CRREL's 500 kg/m³ compaction limit in snow; Alvarado's 0.30 m loose layer), with a rim of `2ν` of the displaced volume, sweeping the path between frames. The cover map's new snow fills a trench by what it gained, melt clamps it, the snow drift and sand flux diffuse it, sand and mud slump (Sumner's angles). The terrain material and `GroundExtension` draw it per pixel: parallax occlusion mapping with per-texel steps and a B-spline bisection, a soft self-shadow, an occlusion term, compacted-snow tint, soil under grass and water on wet rut floors.

Eighteen capture rounds. The defects found and fixed, in order: rims stacking into fins (a plastic rim target), ruts refilled by their own rims, a 14 cm parallax step against 5 cm texels, stair-stepped bilinear walls (B-spline refinement), on/off shadow blocks across sand ruts, mirror-white mud ruts (water only on floors), fluted wet walls (the normal from the B-spline, not a stored gradient), regular dark ticks on a rut's far wall (shadow samples spaced by each pixel's own reach), and grid-aligned slump steps (eight neighbours).

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **Interactive ground trails** | 3 (missing game ↔ world interaction) | picked (user direction, iteration 13) |
| 2 | View-dependent exposure cap | 2 | carried |
| 3 | Flat terrain shading | 2 | carried |
| 4 | Night thunderstorm rain as white static | 1 | carried |
| 5 | Save/restore of the GPU layers | 3 | carried |

### Sources

Sumner, O'Brien & Hodgins 1999; Alvarado et al. 2022; Su et al. 2023; Goswami 2024 (survey); Hanák 2021 (AC III, Batman, Rise of the Tomb Raider, HZD, TLOU II); Svensson 2019; Barré-Brisebois 2014; CRREL 90-9; NASA TM-20250006958 (Wong's Bekker constants); Tatarchuk 2006 (POM, `s2006-advances-materials-10`); Sigg & Hadwiger 2005 (B-spline filtering). Paywalled and not obtained: Onoue 2005, Zeng 2007, Dagenais 2016, Michels (GPU Pro 7), Festenberg 2011.

### Critique and verdicts (`/tmp/trails/critique.md`; rounds `/tmp/trails/r1`–`r18`, final `/tmp/trails/final/`)

| Feature | Evidence | Verdict |
|---|---|---|
| Snow prints and ruts, 10.5 h and 15.6 h | `final/snow.png`, `final/snow_low.png`, `r16/snow_top_crop.png` | GREAT: crisp pockets, blue-shadowed walls, sunlit lips |
| Dry sand at 18.3 h | `final/sand.png` | GREAT: slumped troughs with low rims; the shaded wall's silhouette steps per pixel (no AA, `r17/comb*.png` proves it is not the field) |
| Mud after rain | `final/mud.png`, `final/mud_wall.png` | GREAT: soil floors, wide low rims, no fluting or ticks |
| Dry sand at noon (62°) | `r16/sand_walk_zoom.png` | faint, as real 3.4 cm dishes are; recorded in Validation 7 |
| Snowfall (×120) | `r18/fill.png` | softens at the cover's 1.5 cm/h; a day to fill, not the plan's hour |
| Motion | `docs/trails.gif` (frames 1080–1500) | no swimming, no window seam |

### Verification

- `tests/gpu_trails.rs` (4): the press depth 23.65 cm for the model's 24.0 and a rim of 9.9 % for `2ν` = 10 %; a 20 m/s rut with no texel shallower than 24 cm; 5 cm of new snow shallows a trench 23.65 → 18.64 cm and leaves the flat at 0; a trail stays put as the camera moves 10 m and is gone after it leaves the window. Five unit tests in `trails::model` (sinkage, soil monotone in wetness, rock, window centre, oversized sweep).
- Gates: `cargo build --release`, `cargo test` (0 failures), `cargo build --examples --all-features`, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32, rustfmt on this iteration's files, `tests/save.rs` with `--features serde` (2). GPU, one process per file/module: every `tests/gpu_*.rs` file (trails 4, hydrology 9, precipitation 6, clouds 5, weathered 4, twilight 3, water 2, wind 2, cloud ambient, imports, plugins, readback) and lib `clouds` (10), `terrain` (8), `cover` (7), `dust` (6), `voxel` (1), `the_horizon_sky` — all pass.
- Performance (`bench_elements`, High, hills, `PartlyCloudy`, 1080p, P2 at 1.5–1.9 GHz logged in the same shell, two rounds each): `elements_trails` 0.119 ms with 16 wheels (0.187 ms before untouched texels skipped their ground lookups); `main_opaque_pass_3d` 0.054–0.058 ms off, 0.057–0.058 ms on.
- Stills `docs/trails_snow.png`, `trails_sand.png`, `trails_mud.png` and `docs/trails.gif` (new); `tour.gif` unchanged by this iteration.

### Top 5 remaining items, ranked

1. **A view-dependent exposure cap** (carried): meter the brightest sky in the frame.
2. **Flat terrain shading** (carried).
3. **Night thunderstorm rain** reads as white static (carried).
4. **Trails on `VoxelGround` and the wind's solid mask**: an imprint does not yet change the wind or the hydrology (the field is visual and erodes, it does not drain).
5. **Save/restore of the GPU layers** and the cosmetic set (the aerial's 2-px line, the composite's pillar edge step, `STRIKE_LEAD`, sparse roof splashes, POM silhouettes without AA).

## Iteration 15 — 2026-10-18 — waves: ripples and wakes from bodies in the water

**Commits:** `ec7027d` (research note `docs/research/waves.md`), the implementation with `tests/gpu_waves.rs` (the note's Deviations 1–10 and measured validations), the README / CHANGELOG / stills / GIF, this log. Merged through a pull request. **Sibling repos:** none (no sibling owns open water). **Not released.**

### What changed, and why it ranked first

The hydrology fills lakes and rivers, but nothing that moved through them disturbed them: a boat, a wader or a dropped stone passed through a glassy flow-mapped surface. Trails gave the ground an answer to what crosses it; the water had none. `WavesPlugin` adds it: a `WaterContact` ellipsoid presses the water with the pressure head of its occupied column (Colen & Kolomeisky's pressure-forced oscillator, so a body at rest leaves the water flat around it) plus Bernoulli's impact head while it enters; a linear capillary-gravity field in three camera-following cascades (iWave, a 13 × 13 kernel least-squares fitted to Airy's dispersion per cascade, Canabal's analysis) carries the waves, reflected by the shores, slowed by depth, advected by the current, broken at `ak` = 0.44 into foam. The water's two passes add its slopes and a lace of foam.

Eleven capture rounds. The defects found and fixed, in order: no wake (the example scaled the contact with the hull's mesh), tan hard-edged reflections on wave faces (reflections below the horizon sampled Bevy's ground disc; now held at the horizon), foam a featureless smear (now lace), a falling pebble making no rings (the entry head), a pebble's foam a notched rectangle (five-point binomial and a coverage cap), checkerboards at the forcing (the band split's 3 × 3 binomial).

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **Interactive water: ripples and wakes from bodies** | 3 (missing game ↔ world interaction) | picked: the water is the one simulated surface that ignored what crossed it |
| 2 | Save/restore of the GPU layers | 3 | carried |
| 3 | View-dependent exposure cap | 2 | carried |
| 4 | Flat terrain shading | 2 | carried |
| 5 | Night thunderstorm rain as white static | 1 | carried |

### Sources

Tessendorf 2004 (iWave; *Simulating Ocean Water* course notes); Canabal et al. 2016 (dispersion kernels); Ottosson 2011 (Frostbite's interactive water); Crest (`UpdateDynWaves.compute` and docs); Yuksel et al. 2007 (wave particles); Jeschke & Wojtan 2017 (breaking at `a` = 0.07λ); Schreck et al. 2019; Kass & Miller 1990; Colen & Kolomeisky 2021 (pressure-forced surface, eq. 8); Darmon et al. 2014 (Kelvin's 19.47° for all Froude numbers, the apparent narrowing); `s2006-advances-materials-5/6` (ToyShop's water); `eth-cgl-sim_anim-Sol11b`.

### Critique and verdicts (`/tmp/i15/critique.md`; rounds `/tmp/i15/r1`–`r11`, final `/tmp/i15/final/`)

| Feature | Evidence | Verdict |
|---|---|---|
| Hull wake at 16.5 h | `r11/sheet.png` (side, behind, front, over), `final/gif/` | GREAT: Kelvin's V with transverse crests, silvered by the horizon, lace foam behind the stern |
| Pebble rings | `final/peb_strip.png` | GREAT: short wavelets outrun long ones; the foam dissolves |
| Wader | `r11/wader.png` | GREAT: two short V wakes and flecks |
| Float | `r11/float.png` | right but subtle: 0.7 Hz rings are 3 m swells |
| Window seam | `r11/main_far.png` | none where the 25.6 m window ends |
| Motion | `docs/waves.gif` (frames 1500–1900) | no swimming as the camera rides with the hull |

### Verification

- `tests/gpu_waves.rs` (3): a body at rest holds the water down under it (−3.63 cm) and nowhere else (0.09 mm 3.5 m away), unchanged after the camera moves 3 m; a hull at 2 m/s leaves a wake whose edge stays within 19.1° (Kelvin 19.47°) with a 7.7 cm peak; waves do not cross onto dry ground (4.6 cm on the water, 0 on the bank). Four unit tests in `waves::model` (the kernels' dispersion within 3 % over 3–12 texels and stable, a floating sphere's head, a rotated ellipsoid's chord, a falling body striking only as it crosses).
- Gates: `cargo build --release`, `cargo test` (0 failures), `cargo build --examples --all-features`, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32, rustfmt on this iteration's files, `tests/save.rs` with `--features serde` (2). GPU, one process per file/module: every `tests/gpu_*.rs` file (waves 3, hydrology 9, precipitation 6, clouds 5, trails 4, weathered 4, twilight 3, water 2, wind 2, cloud ambient, imports, plugins, readback) and lib `clouds` (10), `terrain` (8), `cover` (7), `dust` (6), `voxel` (1), `the_horizon_sky` — all pass.
- Performance (`bench_elements`, High, lake, `ELEMENTS_CONTACTS=8`, `PartlyCloudy`, 1080p, P2 at 1.74–1.80 GHz logged in the same shell, two rounds on and off): force 0.037, step 0.217, publish 0.033 ms (0.29 ms; 0.23 ms on still water; Low 0.15 ms), above the note's 0.15 ms and logged as `waves.md` Deviation 10. The GPU was shared with a vLLM server during the runs.
- Stills `docs/waves_wake.png`, `docs/waves_rings.png` and `docs/waves.gif` (new).

### Top 5 remaining items, ranked

1. **The sky darkens toward black over a filled lake at 17.5 h** (found here; identical with `ELEMENTS_WAVES=off`, so not the waves).
2. **Buoyancy readback**: publish the wave height under each `WaterContact` so a float can ride the waves it and others make.
3. **Save/restore of the GPU layers** (carried).
4. **A view-dependent exposure cap** and **flat terrain shading** (carried).
5. **Wave cost**: step the coarse cascades at 30 Hz or skip windows with no body and no waves; refraction of what is under the water (the pebble draws where it is, not where it appears).

## Iteration 16 — 2026-10-18 — the sun under broken cloud: a steady sky under a cloud's shadow, a hidden disc

**Commits:** `3ea0d0f` (research note `docs/research/broken-cloud-light.md`), the implementation with the new GPU test, the README / CHANGELOG / still / tour GIF, this log. Merged through a pull request. **Sibling repos:** none. **Not released.**

### What changed, and why it ranked first

Iteration 15's top backlog item, "the sky darkens toward black over a filled lake at 17.5 h", reproduced on `main` and turned out to be general: the sun's and moon's lights carried the cloud transmittance of the 70 m around the camera, and Bevy lights its sky, aerial perspective and environment map with them, so every cumulus that shaded the camera took the whole sky with it — black in `waves` at 17.5 h (the camera's `T̄` reached 0 and `sky_gain` had nothing to scale), grey in `terrain_tour` under `PartlyCloudy` within seconds of a shadow arriving. A second defect of the same scene: the sun disc burned through a cumulus's dark base, because the march kept the last 1 % of the sky behind an opaque cloud and the disc is 10⁵ times the sky. Category 1 (visible, physically wrong, in the default `PartlyCloudy` of the flagship example); it beats the runner-up (buoyancy readback, category 3) because every game with fair-weather cumulus hit it.

The lights now carry the shadow map's mean over 8 km (Hillaire 2016 eq. 22's hemisphere mean; Kasten & Czeplak's mean over points), computed on the CPU from the 256² grid already read back for `Environment::cloud_shadow_at`; the crate's surfaces keep their per-pixel shadow relative to it. `CloudShadows::camera_transmittance` became `mean_transmittance` (migration note in the CHANGELOG); the camera kernel and its readback are deleted. An exhausted march is opaque (Schneider & Vos 2015). Bevy's `DirectionalLightTexture` would have shadowed every `StandardMaterial` too, but it needs clustered decals, which Metal and WebGPU lack; the `waves` example's bodies became `WeatheredMaterial` so they take the shadow.

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **The sky follows the camera's cloud shadow; the sun shines through cumulus** | 1 (visible artifact) | picked |
| 2 | Buoyancy readback for `WaterContact` | 3 | carried |
| 3 | Save/restore of the GPU layers | 3 | carried |
| 4 | View-dependent exposure cap; flat terrain shading | 2 | carried |
| 5 | Wave cost (coarse cascades at 30 Hz) | 5 | carried |

### Sources

Hillaire 2016 §4.1.2, §5.9, §5.9.2 (local text); Wenzel 2006 (`s2006-advances-materials-12`); Ubisoft Montréal 2025 (`s2025-advances-materials-3`); Patry 2021 (`s2021-advances-pdf-66-mb`); Schneider & Vos 2015 (`s2015-advances-ppt-944-mb-pdf-5-mb-2`); Mol & van Heerwaarden 2025 (`10.5194_acp-25-4419-2025`, downloaded); Mol et al. 2023 (`10.1029_2022jd037894`, downloaded); Kasten & Czeplak 1980 (via `celestial.md`); Bevy 0.20 source (atmosphere light loop, light render layers, clustered-decal light textures). Not obtained: Perez et al. 1993.

### Critique and verdicts (`/tmp/i16/critique.md`; before `/tmp/i16/before/`, after `/tmp/i16/after/`, `/tmp/i16/cmp/`, sequences `/tmp/i16/seq_*`)

| Feature | Evidence | Verdict |
|---|---|---|
| Sky under a cloud's shadow | `after/long_cmp.png` (waves 17.5 h, frames 600–1800), `seq_cmp.png` + `seq.py` (sky 0.9 % range vs 26.6 %), `docs/cloud_shadow_sky.png` | GREAT |
| Sun behind a cumulus | `after/dn_dot_cmp.png` (4× zoom: disc gone, silver lining kept) | GREAT |
| Props in the shadow | `after/props.png` (waves bodies darken with the water) | GREAT with `WeatheredMaterial`; plain `StandardMaterial` sees the mean (documented) |
| No regression | `cmp/all.png` (Overcast, Thunderstorm, night, dusk, Clear), `day_night` 16.5/18.0 h within 0.5 %, `tour_sheet.png` | GREAT |

### Verification

- `tests/gpu_clouds.rs` `a_cloud_over_the_camera_leaves_the_lights_alone` (new): beam 0.5666 in the shade vs 0.5752 in the sun, ambient 11 842 vs 11 607 lx; on `7dd1ef0` it fails (0.0222 vs 0.8591). `the_shadow_map_and_the_sun_factor_follow_the_clouds` now also checks the beam against the map's mean (Clear 0.9696 vs 0.9691). `clouds::model` `an_opaque_column_hides_what_is_behind_it` replaces the early-exit test.
- Gates: `cargo build --release`, `cargo test` (0 failures), `cargo build --examples --all-features`, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32, rustfmt on the changed hunks, `tests/save.rs` with `--features serde` (2). GPU, one process per file/module: every `tests/gpu_*.rs` file and lib `clouds` (10), `terrain` (8), `cover` (7), `dust` (6), `voxel` (1), `the_horizon_sky` — all pass; `gpu_hydrology`'s two bake tests timed out (240 s wall deadline) while a vLLM server held the GPU at 95 % and passed on a rerun with the GPU idle.
- Performance (`bench_elements`, High, hills, `PartlyCloudy`, 1080p, four rounds alternating `7dd1ef0` and this tree, P2 1740–1800 MHz logged in the same shell): `elements_clouds_shadow` 0.066–0.079 → 0.059–0.065 ms, march 0.436–0.466 → 0.443–0.468 ms, composite 0.037–0.038 → 0.042 ms (unchanged code); net ≈ −0.003 ms. CPU mean of the readback grid 47 µs at 4 Hz.
- `docs/tour.gif` regenerated; `docs/cloud_shadow_sky.png` new.

### Top 5 remaining items, ranked

1. **The anti-solar sky at sunset reads dark grey** (`/tmp/i16/cmp/pair_w1_19.6.png`: sun +1.3°, `PartlyCloudy`, looking east; same before and after): the glow-capped exposure leaves the Belt of Venus side nearly black between orange clouds.
2. **Ribbed cumulus faces**: the large cumulus in `terrain_tour` shows horizontal stair-step shelves on its sunlit face (`/tmp/i16/seq_after/`).
3. **Buoyancy readback**: publish the wave height under each `WaterContact` so a float can ride the waves (carried).
4. **Save/restore of the GPU layers** (carried).
5. **A view-dependent exposure cap**, **flat terrain shading**, and **wave cost** (carried).

## Iteration 17 — 2026-10-18 — cumulus that billow

**Commits:** `6930852` (research note `docs/research/cumulus-shape.md`), the implementation with the mirror and tests, the README / CHANGELOG / still / tour GIF, this log. Merged through a pull request. **Sibling repos:** none. **Not released.**

### What changed, and why it ranked first

The review of `terrain_tour` under `PartlyCloudy` from several angles (`/tmp/i17/before/cu_sheet.png`) showed every fair-weather cumulus as a kilometre-wide smooth pancake with stacked shelves on its flank — iteration 16's "ribbed cumulus faces" turned out to be a symptom of a bigger beauty gap visible in every outdoor frame under `Clear` and `PartlyCloudy`. Experiment A (no dome scaling, `/tmp/i17/expA`) proved the shelves are the dome terraces; the slab and the weak, coarse detail erosion made the pancakes. It beats the runner-up (the dark anti-solar sky at sunset, one hour of the day) because it is in every daytime frame.

Cumuliform clouds are now carved by Nubis Evolved's envelope model: a flat base as a factor on the density, a C¹ rounded top following the blob, convective cells from the base noise (closing as the cover reaches 1), and a value erosion `√saturate((N − (1 − D)) / D)` of a billowy Worley composite on retuned tiles (4096 m base, 640 m detail). The detail octaves fade to their mean as the march's step outgrows them. The CPU mirror carries the same model; stratus is unchanged.

Eleven capture rounds. Found and fixed in order: a blob stayed one mass (cell mask, core profile), clouds translucent grey-blue (full density in cores), billows hanging under the base at sunset (base as a density factor), rim grain at 3× (edge exponent ½, detail tile 640 m, a detail band-limit), a Blizzard deck opened into holes (gaps close with `cover⁴`), the coarse march's `pow` cost (a linear sign test).

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **Cumulus as pancakes with shelves** | 2 (+1: terraces) | picked |
| 2 | Anti-solar sky at sunset dark grey | 1 | carried |
| 3 | Buoyancy readback | 3 | carried |
| 4 | Save/restore of the GPU layers | 3 | carried |
| 5 | View-dependent exposure cap; flat terrain shading; wave cost | 2/5 | carried |

### Sources

Schneider & Vos 2015 (`s2015-advances-ppt-944-mb-pdf-5-mb-2`); Schneider & Vos 2017 (`s2017-advances-pdf-slides-13mb`); Schneider 2022 *Nubis, Evolved* and 2023 *Nubis³* (Advances PDFs, downloaded); Bauer 2019 *RDR2* (Advances PPTX, downloaded); Webanck et al. 2018 *Procedural Cloudscapes* (`10.1111/cgf.13373`, HAL); Goswami 2021 cloud survey (`10.1007_s00371-020-01953-y`, downloaded); Toft et al. 2016 (`10.48550_arxiv.1609.05344`, downloaded); Mol et al. 2023. Not obtained: Plank 1969, Neggers et al. 2003, Wood & Field 2011.

### Critique and verdicts (`/tmp/i17/critique.md`; rounds `/tmp/i17/r1`–`r11`, `b2_r7`–`r10`; final `/tmp/i17/final`, `final_b2`)

| Feature | Evidence | Verdict |
|---|---|---|
| Cumulus shape (9 h toward the sun, 15.5 h away, overhead, horizon, aerial) | `final/strip.png`, `final_b2/sheet.png`, `docs/cumulus.png`, `r7/zgc.png` | GREAT: cauliflower turrets, separate thermals, crisp silhouettes, no shelves |
| Sunset undersides | `final_b2/sunset.png`, `b2_r9/zh3c.png` | GREAT; rim sparkle on the horizon row at 4× (half-resolution march), cosmetic |
| Stratus, Overcast, Thunderstorm, Blizzard | `final_b2/sheet.png`, `the_weather_air_makes_the_sky_the_fog_colour` | unchanged |
| Motion | `seq_sheet.png`, frame differences 0.35–0.37 with no spikes over 6 s | GREAT |

### Verification

- `clouds::model`: `the_cumulus_envelope_is_smooth_and_the_erosion_bounded` (C¹ envelope, plane base, monotone bounded erosion), `the_coarse_test_never_misses_a_cloud` (four types, three detail weights), `detail_noise_only_removes_stratus_density`, `the_cumulus_shadow_covers_what_the_weather_says` (shade 0.048 / 0.377 / 0.818 against the map's 0.052 / 0.399 / 0.799); the stratus column still τ 6.04. `tests/gpu_clouds.rs`: the Clear shade fraction in the 8 km window is 0.006 (floor now 0.001, Deviation 9); the camera test picks 164 m blocks ≤ 150 m apart and allows 5 % (measured 3.4 %).
- Gates: `cargo build --release`, `cargo test` (0 failures), `cargo build --examples --all-features`, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32, rustfmt on the changed files, `tests/save.rs` with `--features serde` (2). GPU, one process per file/module: every `tests/gpu_*.rs` file (clouds 6, hydrology 9, precipitation 6, trails 4, weathered 4, twilight 3, waves 3, water 2, wind 2, cloud ambient, imports, plugins, readback) and lib `clouds` (10), `terrain` (8), `cover` (7), `dust` (6), `voxel` (1), `the_horizon_sky` — all pass. `a_flash_lights_the_cloud_at_dusk_and_not_at_noon` now allows 5 % at noon (measured 3.4 %: the cumulonimbus's flat base sits where the flash fires; `cumulus-shape.md` Deviation 10).
- Performance (`bench_elements`, High, hills, `PartlyCloudy`, 1080p, alternating runs against `2791780`, P2 at 1725–1800 MHz logged in the same shell, the GPU otherwise idle): `elements_clouds_march` 0.437–0.474 ms (seven runs) → 0.489–0.492 ms (three runs of the final shader): +6 %, within the note's 10 %; `elements_clouds_shadow` 0.058–0.079 → 0.069–0.072 ms. An `EMPTY_LIMIT` of 6 gained nothing; `pow` → `sqrt` in the erosion and a linear coarse test took the march from 0.521–0.527 to 0.489–0.492.
- Stills `docs/cumulus.png` (new), `docs/tour.gif` regenerated.

### Top 5 remaining items, ranked

1. **Cumulus lighting**: from above (`final_b2/aerial.png`) the tops are tan in the haze, and backlit or front-lit bodies are low-contrast cream-grey rather than brilliant white with darker bases; the multiple-scattering and ambient balance of the march, now that the shapes are right.
2. **The anti-solar sky at sunset reads dark grey** (carried).
3. **Rim sparkle** on distant lit cloud edges at sunset (half-resolution march; a full-resolution edge pass or a bilateral upsample).
4. **Buoyancy readback** and **save/restore of the GPU layers** (carried).
5. **A view-dependent exposure cap**, **flat terrain shading**, **wave cost** (carried).

## Iteration 18 — 2026-10-18 — clouds that are white: multiple scattering by diffusion

**Commits:** `d3d0f8d` (research note `docs/research/cloud-lighting.md`), the implementation with the mirror, the Monte Carlo table and tests, the README / CHANGELOG / still / tour GIF, this log. Merged through a pull request. **Sibling repos:** none. **Not released.**

### What changed, and why it ranked first

Iteration 17's top item. With the shapes right, the lighting was the beauty gap in every outdoor frame: cumulus were translucent grey-blue from the ground, khaki from 3.8 km and black underneath at sunset (`/tmp/i18/before/sheet.png`). A Monte Carlo reference (`docs/research/cloud-lighting-mc.py`: homogeneous conservative slabs, the march's dual-lobe phase, 2×10⁶ photons per case) put numbers on it: three Wrenninge octaves gave 0.16–0.3× of a thick top and nearly nothing of a base. The best octave sum the rule `a ≤ b` allows still had 3× the error of a diffusion term.

The octaves are replaced by single scattering plus photon diffusion between the lit face and the cloud behind, `α √μ_l / π · (τ_b + z) / (τ_l + τ_b + 2z)`, with Jensen 2001's extrapolation `z = 2/(3(1 − g))` (the free fit recovers 3.39 against 3.33) and α = 0.68; `τ_b` is one extra base-shape sample 300 m behind each sample. Two more defects surfaced while prototyping and were fixed: descending rays took their aerial perspective along a path that met the ground (khaki from above; now Bruneton's reversed ray), and the clouds' ambient still carried the sunlight the diffusion now carries, turning the noon overcast cream (the ambient is now the sky's colour alone). The far veil had cloud behind a deck's base and drew a ridge at the 32 km boundary; it has none now.

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **Cloud lighting: grey/khaki cumulus, black bases** | 2 | picked |
| 2 | Anti-solar sky at sunset dark grey | 1 | carried |
| 3 | Rim sparkle on lit cloud edges at sunset | 2 | carried |
| 4 | Buoyancy readback; save/restore of the GPU layers | 3 | carried |
| 5 | View-dependent exposure cap; flat terrain shading; wave cost | 2/5 | carried |

### Sources

Hillaire 2016 *Physically based sky, atmosphere and cloud rendering in Frostbite* (§5.8); Hillaire 2020 SIGGRAPH PBS course slides (Unreal's octaves, albedo ≈ 1 needs multiple scattering); Bouthors et al. 2008 (HAL `inria-00333007`: orders 1–30 carry 30–100 % of the reflectance); Jensen et al. 2001 (`10.1145_383259.383319`, the extrapolated boundary `φ − 2AD ∂φ/∂n = 0`); Kallweit et al. 2017 (`10.1145_3130800.3130880`, downloaded). Not obtained: Lacis & Hansen 1974, Meador & Weaver 1980, Wrenninge 2013.

### Critique and verdicts (`/tmp/i18/critique.md`; rounds `/tmp/i18/p1`, `p2`, `r1`–`r3`)

| Feature | Evidence | Verdict |
|---|---|---|
| Cumulus from the ground (15.5 h, 9 h, 11 h) | `r3/cmp_front.png`, `r3/strip.png`, `r3/cmp_horizon.png` | GREAT: white tops, grey bases, crisp against the blue |
| Cumulus from 3.8 km | `r3/cmp_aerial.png` | GREAT: white over the haze (was khaki) |
| Sunset undersides | `r3/z_sunset.png`, `r3/z_rim.png` | GREAT: orange-lit, volume reads; rim speckle unchanged (carried) |
| Noon overcast | `r1/overcast.png` (cream, NOT YET) → `r3/overcast.png` | GREAT: neutral (226, 225, 222), brighter than (193, 195, 197) |
| Storm, night, overhead | `r3/cmp_storm.png`, `r3/cmp_night.png`, `r3/cmp_e.png` | pass: storm unchanged under its fog, moonlit clouds keep their form |

### Verification

- `clouds::model`: `the_slab_matches_monte_carlo` (96 cases, mean squared log error 0.067 ≤ 0.08; thick tops 0.57–1.12×, bases 0.45–1.19×, `cloud-lighting.md` Deviation 2), `the_integrator_converges_on_the_slab` (Q 64 within 5 % of Q 1024), `the_source_bound_is_the_phase_peak_and_the_diffuse_albedo` (2.7285), the phase integral and values, the stress bound, the white furnace.
- GPU: `the_far_veil_continues_the_marched_deck_at_full_cover` (near rows ratio 0.95–0.97, tint ≤ 0.016, chroma ≤ 0.031; far rows now 0.08 / 0.15, measured 0.062 / 0.122, a smooth drift with distance, Deviation 4); `the_deck_takes_the_colour_of_its_light` (noon blue/red 1.12× the neutral deck, bound now < 1.2; dusk red/green 1.26 against 1.02).
- Gates: `cargo build --release`, `cargo test`, `cargo build --examples --all-features`, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32, rustfmt on the changed files, `tests/save.rs` with `--features serde`, every GPU file and lib module (`/tmp/i13/gpu_suite.sh`).
- Performance (`bench_elements`, High, hills, `PartlyCloudy`, 1080p, alternating runs against `8b0620c`, P2 at 1740–1800 MHz logged in the same shell, the GPU otherwise idle; one run at P5 discarded): `elements_clouds_march` 0.480–0.524 → 0.513–0.539 ms (+3–7 %, within 10 %); `elements_clouds_shadow` unchanged (0.069–0.072 → 0.068–0.073).
- Stills `docs/cloud_lighting.png` (new), `docs/tour.gif` regenerated.

### Top 5 remaining items, ranked

1. **Open issues #7–#15** (filed by a consumer integrating the crate): orthographic cameras and a follow anchor, a world-units scale, opt-outs on `ElementsCamera`, `CelestialPlugin` without its own lights, a public `ElementsGlobals` handle, `bevy_flora` 0.46, the `isomesh` feature for many volumes, serializing `SurfaceWeathering`, a snow season on `Climate`.
2. **The anti-solar sky at sunset reads dark grey** (carried).
3. **Rim sparkle** on distant lit cloud edges at sunset (carried).
4. **The aerial haze floor** seen from 3.8 km is a flat featureless grey (`r3/aerial.png`).
5. **Buoyancy readback**, **save/restore of the GPU layers**, **a view-dependent exposure cap**, **flat terrain shading**, **wave cost** (carried).

## Iteration 19 — 2026-10-18 — a focus for the camera-relative domains, and orthographic cameras (issue #7)

**Commits:** `4de4ed2` (research note `docs/research/view-anchor.md`), the implementation with tests, the `isometric` example, the README / CHANGELOG / still / GIF, this log. Merged through a pull request; closes #7. **Sibling repos:** none. **Not released.**

### What changed, and why it ranked first

The user asked to start from the open issues. Nine were filed today by a consumer (Sanctuary) integrating the crate; #7 ranked first because it blocks the adoption of five domains at once under the camera every isometric and strategy game uses, and #8 (units) touches the same boundaries after it. Under an orthographic camera 1500 m back from what it shows, every camera-relative domain centred 1500 m away and the cloud march built its rays from one eye point.

`ElementsFocus::Entity` / `Point` on the camera now centres the domains (the view carries the focus and the eye separately; the terrain's level of detail stays on the eye, which its vertex shader morphs by). The cloud passes build rays from the view matrices for both projections. The capture review of a new `isometric` example then found five things Bevy measures from the camera's translation that an isometric camera breaks (Deviations 1–5 of the note): the distance fog, the shadow cascades, precipitation drawn at a telephoto's pixel coverage and a box smaller than the view, the ground-fog volume crossed at one slant, and ray origins inside a low deck. Each is adapted only for an orthographic followed camera; a perspective camera is unchanged.

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **#7 orthographic cameras and a focus** | 3 (adoption) | picked |
| 2 | #8 world-units scale | 3 | next |
| 3 | #9 opt-outs on `ElementsCamera`, #10 `CelestialPlugin` without lights | 3 | queued |
| 4 | #14 serialize `SurfaceWeathering`, #15 a snow season | 3 | queued |
| 5 | #11 public globals handle, #12 bevy_flora 0.46, #13 isomesh volumes | 3 | queued |

### Sources

Marschner & Shirley, *Fundamentals of Computer Graphics* §4.3.1 (corpus); *Real-Time Rendering* 4th ed. §4.7.1 (corpus); Puig-Centelles, Ripollés & Chover 2009 (`10.1007/s00371-009-0366-9`) and Puig-Centelles et al. 2011 (`10.4018/jcicg.2011070102`), both fetched from SciDB and uploaded to the home-still inbox; Tatarchuk 2006 (corpus `s2006-advances-materials-6`); Losasso & Hoppe 2004 (corpus `10.1145_1015706.1015799`); Bevy 0.20's own orthographic test, cascade split and atmosphere rays.

### Critique and verdicts (`/tmp/i19/critique.md`; rounds `/tmp/i19/r1`–`r10`, sequences `seq`, `seq2`)

| Feature | Evidence | Verdict |
|---|---|---|
| Clear / PartlyCloudy / Overcast under the isometric camera | `r10/sheet.png` | GREAT: crisp town, sun shadows, soft overcast |
| Rain, Thunderstorm, Snow | `r10/z_rain.png`, `r10/sheet.png` | GREAT: streaks and flakes over the whole view, no box edge; the pale wet ground is pre-existing (same in the perspective town) |
| Fog | `r10/Fog.png` (was white `r7`, a hard line `r8`) | GREAT: a depth fade from the bottom edge |
| Motion | `seq2`, frame differences 0.032–0.051 | GREAT: the box follows the walker, no respawn bursts |
| Perspective unchanged | `fogcheck/cmp.png`, the GPU suite | unchanged |

### Verification

- New tests: `plugin::the_view_centres_on_the_focus` (point, entity, entity without a transform, removed), `clouds::model::orthographic_rays_are_parallel_and_perspective_rays_share_the_eye` (the ray mirror: parallel rays on the focus plane, the camera's plane without a focus), `lightning::visual::an_orthographic_ribbon_keeps_two_pixels_at_every_distance`, `fog::an_orthographic_view_fogs_from_the_bottom_of_its_ground`, `tests/lightning.rs` `strikes_and_thunder_centre_on_the_focus`, `tests/gpu_precipitation.rs` `the_domains_centre_on_the_focus_of_an_orthographic_camera` (rain box, wind window, fog volume centred on a focus 1500 m from the camera; the box follows a 20 m pan with its count), `tests/gpu_cloud_ambient.rs` `an_orthographic_camera_sees_the_deck_a_perspective_one_does` (luminance 1.04×).
- Gates: `cargo build --release`, `cargo test`, `cargo build --examples --all-features`, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32, rustfmt on the changed files, `tests/save.rs` with `--features serde`, every GPU file and lib module.
- Performance (`bench_elements`, High, hills, `PartlyCloudy`, alternating against `8670d02`, P2 at 1530–1740 MHz with the GPU shared by a vLLM job): `elements_clouds_march` 0.524–0.530 → 0.468–0.529 ms, resolve and composite unchanged; no regression.
- Stills `docs/isometric.png`, GIF `docs/isometric.gif` (new).

### Top 5 remaining items, ranked

1. **Open issues #8–#15**, #8 (a world-units scale) first.
2. **Wet surfaces reflect a clear sky under overcast**: `WeatheredMaterial` and the ground reflect Bevy's environment map, which has no clouds, so wet ground under Rain reads as a pale sheen (`r10/town_rain.png`, perspective and orthographic alike).
3. **The anti-solar sky at sunset reads dark grey** (carried).
4. **Rim sparkle** on lit cloud edges at sunset (carried).
5. **Bevy's own sky and aerial perspective are perspective-only** (`uv_to_ray_direction`): under an orthographic camera looking at the horizon the sky is a perspective fan; the crate's clouds are right, Bevy's atmosphere is not.

## Iteration 20 — 2026-10-18 — a game keeps its own look and lights (issues #9, #10, #11)

**Commits:** `ad45762` (research note `docs/research/integration.md`), the implementation with tests, `ELEMENTS_LOOK=game` in the `isometric` example, the README / CHANGELOG / still / GIF, this log. Merged through a pull request; closes #9, #10, #11. **Sibling repos:** none. **Not released.**

### What changed, and why it ranked first

Of the open issues after #7, #8 (a units scale) turned out to touch ten render shaders, 31 metre constants and every transform-reading domain: an iteration of its own, next. #9, #10 and #11 share one theme every art-directed game hits (the crate took over the camera's look, the lights and the globals), so they went first, together. `ElementsCamera` gained five dressing flags (`atmosphere`, `distance_fog`, `volumetric_fog`, `ambient`, `msaa_off`), each inserting and driving only what is on, removing only what the crate put there, and flipped at runtime by a change system; `DepthPrepass` follows the plugins that read it. `CelestialSettings { lights, sky }` keeps `SunState` without spawning or touching lights (`update_sky` computes, `drive_lights` writes). `ELEMENTS_GLOBALS_BUFFER` is public. The capture review then found the game look's rain lit by the crate's 50 klx sun; the particles now take the `Sun`-marked light (the crate's or the game's) and the camera's own ambient, and the cloud shadows follow a game's `Sun`-marked light.

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **#9, #10, #11: the game's look, lights and globals** | 3 (adoption) | picked |
| 2 | #8 world-units scale | 3 | next (wide: ten shaders, every domain) |
| 3 | #14 serialize `SurfaceWeathering`, #15 a snow season | 3 | queued |
| 4 | #12 bevy_flora 0.46, #13 isomesh many volumes | 3 | queued |
| 5 | Wet surfaces reflect a clear sky under overcast | 2 | carried |

### Sources

Patry 2021 (corpus `s2021-advances-pdf-66-mb`: physically based lighting that "can deviate from physical correctness" with artist knobs per weather state); Schneider & Vos 2015 (corpus: "art direct-able" as a requirement); Tatarchuk 2006 (corpus: artist-directable rain as separate pieces); Bevy 0.20's atmosphere extraction, environment-map observer and `Camera3d`'s required `Exposure`.

### Critique and verdicts (`/tmp/i20/critique.md`; rounds `/tmp/i20/r1`, `r2`, `seq`)

| Feature | Evidence | Verdict |
|---|---|---|
| Game look: key light, palette, no fog or sky | `r2/sheet.png` | GREAT |
| Rain and snow on the game look | `r1/sheet.png` (stark white, NOT YET) → `r2/z_rain.png` | GREAT: in the palette's range, walls wet |
| Cloud shadows along the game's light | `seq_sheet.png` | GREAT |
| Default look | `r1` top row, `tests/gpu_dressing.rs` | unchanged |

### Verification

- New tests: `tests/gpu_dressing.rs` — `the_default_dressing_is_unchanged` (the component set, one atmosphere, two lights, the moon disc and stars), `a_bare_camera_without_lights_keeps_the_games_look` (only `DepthPrepass` inserted, no atmosphere, lights, moon or stars; the game's fog, ambient, MSAA and `Sun`-marked light keep their values; `SunState` equal to the lit run's), `flags_flip_at_runtime`; `tests/gpu_weathered.rs` `a_game_material_reads_the_globals_buffer` (a material extension defined in the test binds `ELEMENTS_GLOBALS_BUFFER` and draws the pinned wetness and snow cover to ±1 DN).
- Found by a test: `Camera3d` requires `Exposure`, so the old `insert_if_new(Exposure::SUNLIGHT)` never applied and an `exposure` flag would be meaningless (Deviation 1).
- Gates: build, `cargo test`, examples, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32, rustfmt on the changed files, `tests/save.rs`, every GPU file and lib module.
- Performance: no shader or kernel change on the default path (the cloud view query gains an `Msaa` filter; the precipitation light is a few CPU lines); not re-benchmarked.
- Still `docs/game_look.png`, GIF `docs/game_look.gif` (new).

### Top 5 remaining items, ranked

1. **#8 world-units scale** (ten render shaders, the metre constants, every transform boundary).
2. **#14 serialize `SurfaceWeathering`** and **#15 a snow season on `Climate`**.
3. **#12 bevy_flora 0.46**, **#13 isomesh with many volumes and a game-owned ground**.
4. **Wet surfaces reflect a clear sky under overcast** (carried).
5. **The anti-solar sky at sunset**, **rim sparkle**, **Bevy's perspective-only sky under an orthographic camera** (carried).

## Iteration 21 — 2026-10-18 — the snowpack survives a save, and a snow season (issues #14, #15)

**Commits:** `175fc8b` (research note `docs/research/snow-season.md`), the implementation with tests, `weathered_town`'s November calendar and save/load, the README / CHANGELOG / still / GIF, this log. Merged through a pull request; closes #14, #15. **Sibling repos:** none. **Not released.**

### What changed, and why it ranked here

#8 (a units scale) was ranked first but touches every render shader and transform boundary; #14 and #15 share the snowpack and are what a game with a calendar and saves hits first (Sanctuary's lying snow vanished on load, and a cold autumn night snowed before its "snow from 14 November"). `Climate::snow_season: Option<SnowSeason>` gates the snow presets out of season and carries `Weather::snow_possible` into every reader of the rain–snow split, the GPU kernels included (`snow_fraction_at(t, possible)`), ramping over the hour outside each boundary. `SurfaceWeathering` serializes; a restored pack integrates only when the loaded clock is within an hour of the save, and with a terrain re-seeds the cover maps through a one-off `cover_seed` kernel (point pack × each cell's slope and shelter exposure), dropping a readback in flight across it.

### Candidates ranked

| # | Candidate | Category | Note |
|---|---|---|---|
| 1 | **#14 + #15: the snowpack in a save, a snow season** | 3 (adoption) | picked |
| 2 | #8 world-units scale | 3 | next (wide) |
| 3 | #12 bevy_flora 0.46, #13 isomesh many volumes | 3 | queued |
| 4 | Wet ground reflects the sun under overcast | 2 | carried; seen in this iteration's 11:00 captures |

### Sources

Jennings et al. 2018 (`10.1038/s41467-018-03629-7`, read from the PDF: the 50 % rain–snow threshold averages 1.0 °C and spans −0.4 to 2.4 °C, the transition 2.6–4.6 °C wide, so a physical ramp cannot keep snow out of a near-freezing fortnight: the season is an authored override); Patry 2021 (art knobs per weather state); the repo's climate-weather, snow-cover and wetness notes. Brown & Mote 2009 not retrievable (Pending index).

### Critique and verdicts (`/tmp/i21/critique.md`)

| Feature | Evidence | Verdict |
|---|---|---|
| Daily stills with auto exposure at 21600× | round 1 | FAIL (blown out; added `ELEMENTS_EV100`) |
| 11:00 stills | round 2 | NOT YET (pre-existing wet-ground sun glare in frame; tofu labels) |
| 15:30 stills: 9 Nov differs, 14 Nov identical | `docs/snow_season.png` | GREAT |
| Daily flip-book, flakes vs rain on 2 Nov | `docs/snow_season.gif` | GREAT |
| Save at 15 Nov, load on a fresh app | `/tmp/i21/save/pair.png` | GREAT |

### Verification

- New tests: `tests/snow_season.rs` (no Snow or Blizzard and no lying snow 1–13 November in the cool-temperate climate with seed 1, the same precipitation as without the season, all of it rain; 40 days from 14 November identical to no season; a −3 °C Rain hour is rain outside and snow inside; forced Snow snows outside; the boundary ramps over 23:00–24:00), `tests/save.rs` (`a_restored_snowpack_continues_the_same_snow` 720 updates identical; `a_snowpack_loaded_a_day_later_neither_melts_nor_grows`, against a carried copy that integrates the day; `files_from_before_the_snow_season_load`), `climate::model` `a_snow_season_wraps_the_year_and_ramps_outside_it`, `cover::gpu_tests::gpu_a_restored_snowpack_seeds_the_maps` (open cells at 0.1 m and 20 mm SWE, wetness 0.6, the 100 cells under the roof bare and dry).
- Found by a test: the first seed request was an `Arc<AtomicU64>` bump, which the render world saw before the frame's parameters were extracted; the render frame in flight ran the seed with the previous, empty pack and marked it done (the map stayed bare, `seeded` caught up). The request is now a plain extracted field beside `params.seed`; atomics only report back. Recorded in `AGENTS.md`.
- Gates: build, `cargo test`, examples, clippy `-D warnings`, `cargo doc` `-D warnings`, wasm32, rustfmt on the changed files, `tests/save.rs`, every GPU file and lib module.
- Performance: one multiply per cell in three kernels; the seed is one 8×8 dispatch per load. Not re-benchmarked.

### Top 5 remaining items, ranked

1. **#8 world-units scale**.
2. **#12 bevy_flora 0.46**, **#13 isomesh with many volumes and a game-owned ground**.
3. **Wet ground reflects the sun under overcast** (Bevy's environment map is the clear sky; visible in every wet noon capture).
4. **The anti-solar sky at sunset**, **rim sparkle** (carried).
5. **Bevy's perspective-only sky under an orthographic camera** (carried).

## Iteration 22 — 2026-10-18 — the `flora` feature on bevy_flora 0.46 (issue #12)

**Commits:** the dependency bump with README / CHANGELOG / still, this log. Merged through a pull request; closes #12. **Sibling repos:** none (bevy_flora 0.46.0 is published). **Not released.**

A game on bevy_flora 0.46 built two copies with the `flora` feature (pinned to 0.30), and `ElementsFloraPlugin` wrote resources its trees did not read. 0.46 pins Bevy 0.20, so the bump keeps pin parity. Its `FloraWind`, `GustFront`, `FloraSeason` and `FloraWetness` keep the fields this crate writes; `GustFront` speed and width are now metres under `FloraUnits` (bevy_flora's `units.rs`: fields that size the simulation in metres, positions in world units), which is what `WindGust` carries, so `src/flora.rs` needs no change. No research note: no system changed, and bevy_flora's own units documentation is the source.

### Verification

- Builds and `cargo clippy --all-targets --all-features -D warnings`, `cargo doc`, the examples and the wasm32 library with `flora`; `flora::tests` (2) pass.
- Visual (`/tmp/i22`): `flora_wind` under forced Rain at 15.8 m/s: autumn colour, wet bark; the amplified difference between frames a quarter-second apart outlines every leaf and branch of the near tree (the crowns sway and flutter: the wind reaches the trees). The pale discs at the horizon are the scene's white spheres in fog, as in the 0.30 still. `docs/flora_wind.png` re-captured. GREAT for what this changes (the wiring); the scene itself is unchanged.

### Top 5 remaining items, ranked

1. **#8 world-units scale** (a `FloraUnits`-style `ElementsUnits`; then `ElementsFloraPlugin` maps it onto `FloraUnits`).
2. **#13 isomesh with many volumes and a game-owned ground**.
3. **Wet ground reflects the sun under overcast**.
4. **The anti-solar sky at sunset**, **rim sparkle** (carried).
5. **Bevy's perspective-only sky under an orthographic camera** (carried).

## Iteration 23 — 2026-10-18 — snow on many voxel volumes without a terrain (issue #13)

**Commits:** `3b95667` (research note `docs/research/voxel-volumes.md`), the implementation with tests, the `voxel_walls` example, README / CHANGELOG / still / GIF, this log. Merged through a pull request; closes #13. **Sibling repos:** none. **Not released.**

### What changed

`VoxelSnow` puts `VoxelGround`'s snow slab on any number of volumes with no terrain: one column per voxel in the volume's field space, the same scan, hold, edit rescans and field swaps (the state is shared; `terrain: Option<Entity>` tells the two apart), and a depth per column from the ground-cover map at the column's world position where a crate terrain lies under it, else the point snowpack, times the top's shed-slope exposure, in field units under a moved, yawed and uniformly scaled volume. `VoxelPlugin` runs it even without the terrain, hydrology and cover plugins.

Two crate defects surfaced in the capture review, both at cliffs that the smooth voxel hill never had: `SnowyField` blended a top across a step and floated the slab's underside off the lower columns (now the nearest column across a step of more than a voxel), and the slope exposure read a merlon beside a walk as a 63° slope and shed the walk's snow (now a neighbour more than a voxel away in height is a wall).

The issue's "builds against 0.0.10" is declined with evidence (Deviation 1): bevy_isomesh 0.0.10 is built on Bevy 0.19 and lacks `replace_field` and `IsomeshSystems`, and bevy_isomesh 0.0.11 pins isomesh 0.0.11 exactly, so a widened core requirement could never resolve to 0.0.10. The consumer moves its path checkout to the published 0.0.11.

### Sources

This crate's `voxel-snow.md`, `snow-cover.md` (Fearing 2000's shed angles) and `snow-season.md` (the point pack); bevy_isomesh 0.0.10 (docs.rs) and 0.0.11 (registry) sources; isomesh 0.0.10 and 0.0.11 diffed for every item used.

### Critique and verdicts (`/tmp/i23/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | Base seam, blobby merlons, black wedges | FAIL |
| 2 | Wedges are geometry: the slab across a cliff (crate) and an air sheet under each merlon (example SDF) | NOT YET |
| 3 | Slab sides shaded as stone (painted per vertex); bare notch in each crenel (exposure across a cliff, crate) | NOT YET |
| 4 | `docs/voxel_walls.png`, `docs/voxel_walls.gif` | GREAT |

### Verification

- New tests: `voxel::tests` `two_snow_volumes_get_slabs_and_no_terrain` (two volumes, one moved, turned 0.3 rad and scaled by 2, no `Terrain`, 0.6 m and 0.3 m field-unit slabs), `a_snow_volume_samples_the_cover_map_at_its_world_position`, `a_tilted_snow_volume_gets_no_slab`, `a_volume_with_both_components_is_a_voxel_ground`, `removing_voxel_snow_restores_the_field`; `voxel::model` `snow_columns_are_one_per_voxel_until_the_cap`, `a_top_sheds_by_its_slope` (with the cliff case), `only_an_upright_evenly_scaled_volume_takes_snow`; `voxel::field` `the_slab_sits_on_both_sides_of_a_cliff`.
- Gates: build, `cargo test`, examples, clippy `-D warnings`, `cargo doc`, wasm32 with `isomesh`, rustfmt, `tests/save.rs`, the voxel GPU tests.

### Top 5 remaining items, ranked

1. **#8 world-units scale**.
2. **Wet ground reflects the sun under overcast**.
3. **Bright snow scenes**: the illuminance-metered exposure renders a sunlit snowfield and the stone on it pale (`voxel_walls`).
4. **The anti-solar sky at sunset**, **rim sparkle** (carried).
5. **Bevy's perspective-only sky under an orthographic camera** (carried).

## Iteration 24 — 2026-10-18 — a world-units scale (issue #8)

**Commits:** `0717357` (research note `docs/research/units.md`), `9628f11` (the core: `ElementsUnits`, the metre-frame view, the atmosphere scale), the boundary conversions with tests, the examples' `ELEMENTS_UNITS`, README / CHANGELOG / AGENTS / still, this log. Merged through a pull request; closes #8. **Sibling repos:** none. **Not released.**

### What changed, and why it ranked first

The last open issue, and the one that blocked a foot-unit game outright (every metre size 3.28× too small, the sky's aerial perspective 3.28× too strong). `ElementsUnits { units_per_metre }` keeps the crate simulating in metres in a frame of world ÷ u and converts at the boundaries only: the followed view (one place every camera-relative domain reads), each `Transform` read (`to_metres`), what the crate writes into Bevy's world (the spawned `Atmosphere`'s scale, `DistanceFog`, the ground-fog volume, cascades, the bolt and its light at intensity × u², terrain and water patches and bounds), the public positions, and the 13 shaders that take world positions from Bevy (a `units` factor in each pass's own uniform; `ElementsGlobals` 80 → 96 B). Two parallel slices did the ~120 CPU and 13 shader boundaries against a written contract (`/tmp/i24/contract.md`, from an inventory of both sides).

### Sources

Gregory 2018 (MKS simulations, the world unit the game's choice; corpus), Lagarde & de Rousiers 2014 §4.1–4.2 (homogeneous distance units, `E = I/d²`, 1 unit = 1 m; the author's PDF), Bevy 0.20's `world_to_atmosphere` (the entity's scale), bevy_flora's `FloraUnits` rule.

### Critique and verdicts (`/tmp/i24/critique.md`)

| Scene | Metres vs feet | Verdict |
|---|---|---|
| Hills, partly cloudy | RMSE 0.26 %, ×20 diff: random pollen only | GREAT |
| Hills, rain | RMSE 1.0 %, random drops only | GREAT |
| Cumulus at 16:00 | RMSE 0.33 %, same clouds and haze | GREAT |
| Scripted strike | Same flash glow; bolt hidden by rain on main too | GREAT for the change |

### Verification

- `tests/gpu_units.rs` `a_world_in_feet_answers_as_the_same_world_in_metres` (hills, wind, overcast; heights within 2.3e-5 m, wind within 0.12 %, the same cloud shadows); `tests/lightning.rs` `a_strike_in_a_world_of_feet_is_a_thousand_metres_off`; `precipitation::tests::the_rain_box_is_sixty_metres_in_a_world_of_feet`; unit tests in fog, celestial, clouds (view ray), query (terrain height in feet).
- The full GPU suite at the default 1.0 (every converted shader is a no-op there) and all gates.
- Found during capture: `common::camera` bundling a `Projection` panicked `storm`; the planes are scaled by a system instead.

### Top 5 remaining items, ranked

1. **Wet ground reflects the sun under overcast**.
2. **Bright snow scenes** (the illuminance-metered exposure on a sunlit snowfield).
3. **A bolt hidden by heavy rain at 1.5 km** in `storm` (the scene or the bolt's brightness against the rain veil).
4. **The anti-solar sky at sunset**, **rim sparkle** (carried).
5. **Bevy's perspective-only sky under an orthographic camera** (carried).

## Iteration 25 — 2026-10-18 — the clouds in the environment map (sky light and reflections under cloud)

**Commits:** `e561f09` (research note `docs/research/sky-light.md`), `cac69fa` (fix: the crate's atmosphere planet centre, iteration 24's regression), the sky probe with its tests, docs and images, this log. Merged through a pull request. **Sibling repos:** none. **Not released.** The GPU was taken through `gpu-tenant claim coding --who bevy_elements@big` (olmOCR held 15.5 GB; the examples ran out of memory without the lease).

### First: a regression from iteration 24

The baseline's `waves` under Clear showed a beige dome of radial streaks instead of a blue sky, reflected in the pond, and the partly cloudy town an orange glow. Bisected to `9628f11`: the crate's `Atmosphere` got `Transform::from_scale(u)`, and Bevy reads that entity's `GlobalTransform` as the planet's centre (it places a default one `R` below the origin, which the `Transform` then overwrote). Fixed with `plugin::planet_transform` and a test that fails on the old transform; AGENTS.md trap; `units.md` Deviation 1. The iteration-24 GPU suite and feet/metre captures had passed because they never looked at this sky.

### Candidates (no open issues)

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | Glossy surfaces mirror a clear sky under cloud; overcast water brighter than its sky `(230,238,240)` under `(226,226,226)` | 1 (physically wrong) | **this iteration** |
| 2 | Bright snow scenes under the illuminance-metered exposure | 2 | carried |
| 3 | A bolt hidden by heavy rain in `storm` | 2 | carried |
| 4 | Anti-solar sunset sky, rim sparkle | 2 | carried |
| 5 | Bevy's perspective-only sky under an orthographic camera | 4 | carried |

Rank 1 beat rank 2 because it is wrong in every cloudy preset on every glossy surface (water, puddles, wet ground, `WeatheredMaterial`), where the exposure item touches one scene class. It is the backlog's "wet ground reflects the sun under overcast": the cause was not the sun but the uniform cloud `AmbientLight`, whose specular term Bevy shades with the same boost as its diffuse (2.2× the sky in water at grazing), plus a clear-sky environment map.

### What changed

- **The sky probe.** `march.wesl`'s second entry point marches the clouds into a cube of the atmosphere cube's size (128²) around the followed camera, composes it with Bevy's cube as the screen composite composes the sky (`common.wesl::sky_through_clouds`, now shared), and the camera's `GeneratedEnvironmentMapLight` is handed the composed cube once every texel has been marched (`SkyProbe`, an `Arc<AtomicU32>` handshake). One texel of every 4×4 block per face a frame at High, half the screen's steps, blended into a ping-pong history. The ray march itself became `integrate()`, shared by the screen and the cube; the screen march now computes the sky irradiance once per ray instead of twice.
- **The cloud ambient is retired on the probe camera** (`celestial::update_ambient`): the cube's dome is that light, shaded over normals by Bevy's irradiance map (top : wall : underside 1 : 0.67 : 0.37).
- **The Moon & Spencer shade factor became a relief cue** (`surface_cover.wesl::overcast_relief`, terrain and weathered): the first build darkened walls twice (a metal box's side 86 DN); removing it outright flattened the overcast relief from the air; dividing out the dome's own cosine keeps the −6.5 % relief cue without the double shading (`snow-cover.md` Deviation 16).

### Sources

Hillaire 2016 (Frostbite; clouds in every view including the environment map, eq. 22), Lagarde & de Rousiers 2014 §4.9 (the distant light probe refreshed with the weather; clear 8000, overcast 2000 cd/m²), Epic's *Sky Lights* documentation (Real Time Capture, 128² cube, 9-frame time slicing, PS4 costs), Bauer 2019 via imgeself 2020 (RDR2's per-frame sky cube with clouds), Karis 2013 (split sum, `/tmp/i25/pdf/karis2013.pdf`), Kittler & Darula 2014 (`10.2478/brj-2014-0012`, downloaded: Kähler's 2:1 and 3:1 overcast gradations, `L_Z/E`), Schneider & Vos 2015, Patry 2021, Kasten & Czeplak 1980, Bevy 0.20 source.

### Critique and verdicts (`/tmp/i25/critique.md`)

| Scene | Before → after | Verdict |
|---|---|---|
| Pond, PartlyCloudy | uniform blue water → cumulus and blue gaps in the water; temporal d2/d1 1.39 (motion, not flicker) | GREAT |
| Pond, Overcast / storm | water brighter than its sky → `(194,209,211)` at grazing under `(226,226,226)` | GREAT |
| Town, Overcast / Rain | white veil, metal box glowing on every face → colours read, metal mirrors sky on top (194) and ground on its sides (98) | GREAT |
| Terrain, Overcast (6 m, 120 m) | one washed tone → saturated, haze layers; relief cue kept (`cmp/relief2.png`) | GREAT |
| Snow (6 h grown) | snow brighter than its sky → `(217,222,227)` under `(224,227,230)` | GREAT |
| Dusk, night | no blowout, no black frame | GREAT |
| Clear | unchanged (probe ≡ atmosphere cube: 0.0000) | GREAT |

### Verification

- `tests/gpu_sky_probe.rs`: without cloud the probe is the atmosphere cube (worst relative difference 0.0000); overcast noon E/model 0.76 (±35 %), `L_Z/E` 0.343, wall 0.67 and underside 0.37 of a top through Bevy's irradiance map, ambient 0; partly cloudy cloud share above 10° 0.373 at cover 0.40, Bevy's cube 0.000.
- `plugin::tests::the_crate_atmosphere_centres_the_planet_below_the_origin` (fails on iteration 24's transform).
- `gpu_clouds::the_shadow_map_and_the_sun_factor_follow_the_clouds` and `a_cloud_over_the_camera_leaves_the_lights_alone` lost their ambient assertions (the ambient is 0 on the probe camera; the model's share stays covered by `celestial::tests::the_ambient_carries_the_diffuse_share_and_nothing_at_night`).
- Full GPU suite, cargo test (276), clippy, doc, wasm, save: pass.
- Bench (`bench_elements`, 3840 × 2160, PartlyCloudy, P2 1680–1770 MHz, minimum): `elements_clouds_sky_probe` 0.047 / 0.062 / 0.155 ms (Low / High / Ultra); first build 0.189 ms at High. Screen march 0.883 → 0.869 ms; clouds total 1.418 → 1.474 ms at High.

### Top 5 remaining items, ranked

1. **The overcast dome is flat** (zenith 1.08 × the horizon; Kähler and CIE 2:1–3:1): Eddington's emergent radiance `(2 + 3μ)/5` in `cloud_scatter`'s diffusion term, checked against the fogged horizon (`clouds.md` Deviation 29). `sky-light.md` Deviation 7.
2. **Bright snow scenes** (the illuminance-metered exposure on a sunlit snowfield); the overcast is now metered 0.4 EV above what the dome lights (`sky-light.md` Deviation 8).
3. **The water's body colour under a grey sky** (the storm pond's teal mid-distance `(160,189,197)`, pre-existing).
4. **A bolt hidden by heavy rain at 1.5 km** in `storm`.
5. **The anti-solar sky at sunset**, **rim sparkle**, **Bevy's perspective-only sky under an orthographic camera** (carried).

## Iteration 26 — 2026-10-18 — a look for rain and snow under far-off cameras (issues #25, #23, #27)

**Commits:** `0f8ead5` (research note `docs/research/precipitation-look.md`), the implementation with its tests, the examples, docs and images, this log. Merged through a pull request that closes #25, #23 and #27. **Sibling repos:** none. **Not released.** The GPU was taken through `gpu-tenant claim coding --who bevy_elements@big` (olmOCR's vLLM held 15.2 GB and distill 7.5 GB).

### Candidates (open issues first)

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | #25: rain under an orthographic camera reads as sleet (streaks 63° from vertical at 35° pitch), snow as identical dots | 2 (looks wrong to the consumer) | **this iteration** |
| 2 | #23: `SurfaceWeathering` has no `PartialEq`, so a game's save struct cannot derive it | 3 (API) | **this iteration** (small) |
| 3 | #27: no forecast of the scheduler's coming presets | 3 (API) | **this iteration** (small) |
| 4 | #26: trails on a game's own ground without the crate's `Terrain` | 3 (feature) | next iteration (a larger change: the trail window without terrain layers) |
| 5 | The overcast dome is flat | 2 | carried |

#25 beat #26 because every strategy or isometric consumer sees it in every wet preset; #23 and #27 are a few lines each with clear acceptance tests and rode along.

### What changed

- **`PrecipitationLook`** (`src/precipitation/look.rs`): fall and wind scales on the rain streak, a tint and opacity, a log-normal flake size (salt 5, Box–Muller, z clamped ±2) and a minimum flake width, packed into two `vec4`s of the draw's `DrawParams`; draw-only, defaults the identity. `readable()` (fall ×4, wind ×0.5, tint `(0.85, 0.9, 1.0, 0.7)`, σ 0.5, 1.5 px). CPU mirrors of the streak and the size factor carry the acceptance tests.
- **`WeatherDirector::forecast`**: from the director's `last_hour` and `scheduled` (a fresh chain starts Clear at the clock's hour, a clock jump restarts it, as `run_weather` does), catches the chain up to the clock's hour and runs `schedule` on for the requested hours, without touching the director.
- **`SurfaceWeathering: PartialEq`**: manual, every field but the transient `restored`; `tests/save.rs` now asserts whole-struct equality after a serde round trip.
- **Examples:** `ELEMENTS_PRECIPITATION=readable` in the harness, `ELEMENTS_PITCH` in `isometric`; the game look brightens its streaks against its own key light.

### Sources

Tatarchuk 2006 (corpus `s2006-advances-materials-6`: artist-set rain direction and speed, random particle size), Lagarde 2012 (*Remember Me*: rain should not match reality in bright scenes; slant by art, not wind), Wang & Wade 2004, Garg & Nayar 2007, Tiira et al. 2016 (`10.5194/amt-9-4825-2016`, downloaded: exponential snow PSD), Langer et al. 2004, `view-anchor.md` Deviation 3.

### Critique and verdicts (`/tmp/i26/critique.md`)

| Round | Scene | Finding | Verdict |
|---|---|---|---|
| 1 | Thunderstorm, tint α 1.3 | an opaque white wall (153 DN vs 140) | FAIL |
| 1 | Snow | a few 4–5× soft blobs (log-normal tail) | FAIL |
| 2 | game look, α 0.7 | rain within ~8 DN of the sunlit grass, nearly invisible | NOT YET |
| 3 | brighter crate tint | crate-lit town greyed into a veil; reverted, the game sets its own tint | NOT YET |
| 4 | Rain / game Rain / Snow / game Snow / dusk at 35° | upright 15° streaks that read as rain; flakes of many sizes; town legible | GREAT |
| 4 | Defaults | iso Rain and Snow mean colour within 0.02 DN of before; same dashes and dots | GREAT |

### Verification

- `look::tests`: streak angle 63.4° physical → 15.3° readable at 35° pitch; flake CV ≥ 0.3 with the mean within 5 %; the default is the identity. `weather::tests::the_forecast_is_the_weather_that_comes` (fresh chain and mid-chain under a forced Thunderstorm, director unchanged). `tests/save.rs` (5).
- Full GPU suite, cargo test, clippy, doc, wasm, save: pass.
- Bench (`bench_elements`, High, 3840 × 2160, Thunderstorm, P2 1695–1755 MHz, minimum, two runs each): `elements_precip_draw` 0.122 / 0.126 ms before, 0.123 / 0.115 ms readable.

### Top 5 remaining items, ranked

1. **#26: trails on a game's own ground** without the crate's `Terrain`.
2. **The isometric town at night is black** (22 h, `Rain`: mean 1 DN, max 8, under both looks).
3. **A 25 mm/h storm hides the whole town under an orthographic camera** (both looks; the storm's coverage and the linear fog from the nearest ground).
4. **The overcast dome is flat** (Eddington's emergent radiance in `cloud_scatter`), carried.
5. **Bright snow scenes under the metered exposure**, **the water's body colour under a grey sky**, **a bolt hidden by heavy rain**, carried.

## Iteration 27 — 2026-10-18 — trails on a game's own ground (issue #26)

**Commits:** `c81ac03` (research note `docs/research/trails-game-ground.md`), the implementation with its tests, the examples, docs and images, this log. Merged through a pull request that closes #26. **Sibling repos:** none (`TrailGround` takes `bevy_flora`'s `FloraGround` shape by convention, not by dependency). **Not released.** The GPU was taken through `gpu-tenant claim coding --who bevy_elements@big`; olmOCR's vLLM reloaded during the lease and left 0.9 GB, so the llama-swap models were evicted again as a grant does (logged on the board).

### Candidates (open issues first)

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | #26: trails need the crate's `Terrain`; a game with its own ground (Sanctuary) gets none, no query, and the materials fade every trail under a camera that stands back from its focus | 3 (missing feature) + 1 (a wrong result: the fade by the camera) | **this iteration** |
| 2 | The isometric town at night is black | 1 | carried |
| 3 | A 25 mm/h storm hides the town under an orthographic camera | 2 | carried |
| 4 | The overcast dome is flat | 2 | carried |
| 5 | Bright snow scenes under the metered exposure | 2 | carried |

#26 beat the black night because it is the only open issue, a consumer's blocked gameplay decision (prints at dawn show where flankers went), and it carried a real bug: every trail under the isometric camera was faded out.

### What changed

- **`TrailGround`** (world XZ → world height, soil or `with_sand()`): the kernels take a uniform ground from `SurfaceWeathering` (snow depth and density, wetness), the contact is tested on the CPU against the game's height, and the terrain and cover bindings hold placeholders; `TrailGpu` owns the handles it binds, so the render world no longer needs `TerrainGpu` or `CoverGpu`.
- **`WeatheredExtension::with_trails()`**: the same parallax march as `GroundExtension` (shared `trails::surface`), the window, its centre, the beam's share and the snow depth from two new `vec4`s in `ElementsGlobals` (96 → 128 B, rewritten in place, so the moving centre re-prepares nothing), the field at bindings 104/105, no storage buffer.
- **The window centre is published** (`TrailState::centre`) and every material fades by it, not by the eye: the fix for the focus.
- **`Environment::trail_depth`**: a fifth kernel (`trail_readback`, its own layout and pass) writes the deepest point per 2 × 2 texels with the window origin in a header; `TrailSnapshot` places it.
- **`TrailSettings::texel_m`**, **`SurfaceWeathering::with_snowpack`** (replacing the crate's test-only constructor).
- Examples: the `isometric` walker presses prints into the town's own ground (8 cm texels, `ELEMENTS_SNOWPACK`, `ELEMENTS_TRAIL_TEXEL`); `trails` with `ELEMENTS_TERRAIN=mesh` (a game's hilly mesh, no terrain); `bench_elements` with `ELEMENTS_TERRAIN=game`.

### Sources

Barré-Brisebois 2014 (GDC, *Deformable Snow Rendering in Batman: Arkham Origins*: "we don't have terrain", snow surfaces a game marks, replenish while snowing; slides read as text), Tran 2018 (Kodeco, a world-space trail map projected onto the ground material), Gawron 2018 and `roymasad/Unreal-Snow-Deformation` (surfaces opt in), Hanák 2021 (RotTR's window, TLOU2's parallax prints on any mesh), Tatarchuk 2006 (`s2006-advances-materials-10`), Alvarado et al. 2022 (`10.48550_arxiv.2209.10215`), CRREL 90-9, `bevy_flora` 0.46 `FloraGround`. `distill_search` through the MCP bridge returned 404; `hs distill search` worked.

### Critique and verdicts (`/tmp/i27/critique.md`)

| Scene | Evidence | Verdict |
|---|---|---|
| Game mesh, snow, 2–20 m | `r2/ms3.png` | GREAT: crisp pockets, sky-blue far walls, sunlit lips; equal to the terrain path |
| Game mesh, sand 18.3 h / mud | `r2/sd3.png`, `r2/z_mud.png` | GREAT (trail); the example's mud art is a plain wet colour |
| Isometric, 17.3 h, against the light | `r3/e1.png`, `r3/e3.png`, `final/iso_eve.png` | GREAT: the walker's whole loop a dotted ellipse of shadowed prints at 1× |
| Isometric, dawn behind the camera / noon | `r1/z6_both.png`, `r3/n1b.png` | faint, by physics (down-sun, tonemapper shoulder); Deviation 6 |
| Terrain path | `r2/terrain_snow.png` vs `before/trails_snow.png`; `gpu_trails` 4/4 | unchanged |

Found and fixed in review: mesh mode forcing Clear before the harness grew the snow; the weathered snow bared everywhere when a game pins `snow_cover` without a depth (`gpu_weathered` caught it).

### Verification

- `tests/gpu_trail_ground.rs` (4): 20 m walk, `trail_depth` 0.0700 m at all 39 points, `Some(0)` beside, `None` outside, no `Terrain`; contact on a 10 % slope (0.0690 m on the ground, 0 from 0.3 m above); 5 cm more snow 6.90 → 1.90 cm; a weathered plane draws the print (129, 166, 214) against (234, 231, 231) only `with_trails()`. `trails::tests` (readback decode, a moved window refused).
- Full GPU suite, cargo test, clippy, doc, wasm, save: pass.
- Bench (`bench_elements`, High, 1080p, P2 1755–1800 MHz, minimum): `elements_trails` hills 0.119–0.121 ms (before 0.119), game ground 0.112–0.116 ms; `elements_trails_readback` 0.024–0.026 ms on its 4 Hz ticks; `main_opaque_pass_3d` with the ruts filling much of the frame on a weathered plane 0.413 ms against 0.226–0.236 ms off.

### Top 5 remaining items, ranked

1. **The isometric town at night is black** (22 h, `Rain`: mean 1 DN), carried.
2. **A 25 mm/h storm hides the whole town under an orthographic camera**, carried.
3. **Bright snow scenes under the metered exposure**: snow at 236 DN sits in the tonemapper's shoulder, which also hides prints at noon (`trails-game-ground.md` Deviation 6), and house shadows on snow read a saturated sky blue.
4. **The overcast dome is flat** (Eddington's emergent radiance in `cloud_scatter`), carried.
5. **A compacted-ground material stage for `WeatheredMaterial`** (Barré-Brisebois's two stages: a trench floor through grass shows soil on the terrain, the game's colour on a mesh), and **the water's body colour under a grey sky**, carried.

## Iteration 28 — 2026-10-20 — the ground has surface detail at a metre

**Commits:** `7342d28` (research note `docs/research/ground-detail.md`), the implementation with
its tests, docs and changelog, this log. **Sibling repos:** none. **Not released.** The GPU was
taken through `gpu-tenant claim coding --who bevy_elements@big`. The pass began with a cloud
iteration (the overcast dome's gradation); the invoking prompt redirected it to the ground, and
that work was reverted, diagnosed and recorded under "Deferred" below.

### Candidates (open issues first: none open)

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | The ground is a flat wash at every eye height: near-patch standard deviation **1.07 DN** of 255 at 6 m, 1.64 at 2 m, 1.81 on sand | 2 (beauty gap) | **this iteration** |
| 2 | The isometric town at night is black (22 h, Rain: mean 1 DN) | 1 | carried |
| 3 | A 25 mm/h storm hides the town under an orthographic camera | 2 | carried |
| 4 | The overcast dome is flat (1.08:1 against CIE's 2:1–3:1) | 1 | attempted, deferred (below) |
| 5 | Bright snow scenes under the metered exposure | 2 | carried |

Rank 1 beat the black night because it is in every daylight frame of every outdoor game, at the
distance a player spends the game looking at; the night is one preset at one hour.

### What changed

- **Two band-limited octave scales** in `terrain::common::ground_detail`: a coarse 1.25 m Perlin
  sum (4 octaves, ±20 %) for the mid-distance, a fine 9 cm sum (3 octaves, decorrelated seed,
  ±85 %) for the near field. Each octave is cancelled once the pixel footprint cannot resolve it
  (Lagae et al. 2010 §5.2.1, frequency clamping), replacing the old single 0.8 m⁻¹ band faded by
  a 40–120 m distance constant. `detail_keep` and `detail_scale` are the pieces.
- **`noise::perlin2_d`**: 2D gradient noise in XZ with its analytic derivative. The detail cannot
  use the 3D noise at the surface point: a 3D slice on a tilted surface is not band-limited and
  streaked the cliff faces with diagonal bands (recorded as Deviation 1).
- **The detail fades with the surface's upness** (`smoothstep(0.55, 0.9, n.y)`) as well as the
  footprint, and its footprint is the XZ span divided by the upness: the detail is a function of
  XZ, so a slope's compressed projection must not under-filter it (Deviations 2 and 3).
- Both `TerrainExtension` and `GroundExtension` use it, so a game's own ground gets the same
  surface.

### Sources

Lagae et al. 2010, *A survey of procedural noise functions* (corpus
`10.1111_j.1467-8659.2010.01827.x`): §5.2.1 frequency clamping, §5.2's "a 3D noise sampled at a 2D
surface will not result in a band-limited texture", Cook & DeRose's finding that Perlin is weakly
band-limited. Andersson 2007 (corpus `10.1145_1281500.1281668`): the defect ("lack of detail
either up close, in a distance, or at high angles") and the texture-based fix this note replaces
with procedural bands. Perlin 1985 (via Lagae §2), Musgrave et al. 1989, Galin et al. 2019 (via
`terrain.md`), `terrain.md` Model 2 and Deviation 19 (the replaced constants).

### Critique and verdicts (`/tmp/i28g/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | 3.47 DN near-field (target 4); the 15–40 m band softer than both sides; grain organic | NOT YET |
| 2a | black speckles: the clump remap drove the albedo below zero | FAIL |
| 2c | sand a halftone grid, rock a woven fabric at 4× (single Worley lattice) | FAIL |
| 2d–2e | diagonal streaks on the cliff faces: a 3D noise slice is not band-limited on a slope | FAIL |
| 2f | streaks gone by the upness gate; grass fell to 1.98 DN | NOT YET |
| 2h | **sd 4.26** grass, 5.86 dunes, 5.55 plains, 6.51 grazing; means within 1 DN; no streak, no speckle, no flicker | **GREAT** |

### Verification

- `terrain::model::mirror::tests::the_detail_band_limit_keeps_resolved_octaves_and_cancels_the_rest`:
  kept whole at half the feature, zero at the feature, monotone and continuous across the falloff.
- Distance invariance: far-ground mean 161.019 → 161.017 DN (`hills_6m_noon`), 153.681 → 153.681
  (`hills_aerial`); snow covers the detail (`trails` `ELEMENTS_TERRAIN=mesh` under Snow).
- Temporal: consecutive static frames RMSE 0.013 % of range.
- Full gate: `cargo test` 251, clippy, doc, wasm, the GPU integration suite (one process per file
  and all 19 files), the lib GPU modules clouds/terrain/cover/dust (32) and `the_horizon_sky`
  alone: all pass.
- Bench (`bench_elements`, High, 1920 × 1080, hills, minimum): **0.046 ms** at P2 1710 MHz,
  against the baseline build's **0.058 ms** at P2 1620 MHz in the same session — the new detail
  is *cheaper*, because the old expression evaluated `fbm3` and `perlin3_d` at three octaves each
  and a 2D derivative chain per octave costs less. No regression to justify.
- `nvidia-smi` logged in the same shell: P2, 1680 MHz idle; 1710 during the new run, 1620 during
  the old.

### Deferred: the overcast dome's gradation (attempted first, reverted)

The first half of this pass built Eddington's emergent intensity `(2 + 3μ)/4` for the deck's
radiance (unit hemispherical mean, so the irradiance, exposure and `illuminance_at` are
unchanged). The CPU mirror confirmed the deck grades 3.36:1 and the Monte Carlo slab error
improved 0.0674 → 0.0669; the GPU composed cube read 1.08 → 1.34 (target 1.8–3.2) with
`L_Z/E` 0.344 → 0.383 (inside the CIE 0.382–0.409 band) and `E/model` 0.76 → 0.77. **Blocker,
diagnosed:** the deck grades correctly but the *composite* dilutes it, because
`sky_through_clouds` adds the clear-sky cube at `sky_gain ≈ 126×` under a deck and at grazing
that constant (~1900 cd/m²) flattens the measured ratio. The next attempt must fix the grazing
band's compositing before the gradation can be measured. Files: `/tmp/i28/findings-cloud.md`.

### Top 5 remaining items, ranked

1. **The isometric town at night is black** (22 h, Rain: mean 1 DN), carried three iterations.
2. **The overcast dome is flat**, with its blocker now diagnosed precisely (the `sky_gain` leak at
   grazing); carries its Eddington implementation and measurements in `/tmp/i28`.
3. **A 25 mm/h storm hides the whole town under an orthographic camera**, carried.
4. **Rock has no material detail of its own**: the ground's grain is gated off the steep faces,
   which are left with only the mask's colour and the terrain's geometry. Strata, cracks and a
   rock-specific bump are the natural next piece, and `terrain.md` has no note for it.
5. **The ground's large-scale variation** (patches of longer and shorter grass, bare earth,
   trampled areas) — the masks vary only with slope and sediment, so a plain is one colour at
   200 m. A low-frequency, contrast-only modulation driven by the same noise would fix it.

## Iteration 29 — 2026-10-20 — rock has a material, and the ground has a place in it

**Focus:** the ground, the earth, the mountains — the stuff that's stood on.

**Commits:** `578b9e1` (the research note `docs/research/rock-material.md`), `11b1490` (the
implementation), `71794da` (the split: the turf off the faces, the rock's material), `c0871aa`
(the rock's colour, the relief and the scree), `ebc5c9b` (the docs, the changelog and the
showcase images), this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | The rock faces were a woven herringbone (coherence 0.71 at a fixed 43.5° world direction) and the rock had no material of its own (one flat colour, saturation 15.6 against the grass's 54.8) | 1 | **this iteration** |
| 2 | The plain at 6 m was one uniform green — the layer masks vary only with slope and sediment | 2 | **this iteration** (the same change) |
| 3 | The slope band's soil drew a red-brown contour line round every rock | 1 | **this iteration** (the same change) |
| 4 | The isometric town at night is black (22 h, Rain: mean 1 DN) | 1 | carried |
| 5 | The overcast dome is flat (1.08:1 against CIE's 2:1–3:1), with its blocker diagnosed in iteration 28 | 1 | carried |

Rank 1 beat rank 4 because it is in every outdoor frame of the game the crate ships in — a
player looks at rock whenever the terrain is steep, and the mountains *are* the domain the focus
names. Rank 2 and 3 turned out to be the same defect family (the layer masks) and were taken
with it.

### What changed

- **The ground's turf and the rock's material are two different noises.** The turf — grass, soil
  and sand — is the crate's 2D noise in the world XZ plane, which is the *projection* of the
  surface onto that plane, and it is right on the near-horizontal surfaces where the plane's
  projection is the surface. It must not reach a slope: the plane's lattice is stretched along
  the slope and shows through as a weave at a fixed world direction. So the turf is gated off
  (`turf_gate`, closed at 37°, open at 23°) and the rock's ramp opens at 30°, and the rock's
  material is the crate's **3D noise at the 3D point**, which has no projection at all.
- **The rock has a material:** beds on a *tilted* plane, a second warmer bed tone, a fracture
  network of ridged noise (a Worley cell network bounds cells and read as a giraffe's hide), the
  rock's own grain, and a relief so the light does work on the face.
- **The ground has a place in it:** a low-frequency field takes soil from the grass, so a plain
  gets patches of bare earth. Andersson's Overlay detail cannot do this — it "does not affect
  areas where the base procedural mask is 0.0 or 1.0", and a plain's grass mask is 1 everywhere.
- **The slope band's soil is broken into patches and tapers into the rock**, so the warm brown
  that ringed every grass patch is gone.

### Sources

- **Lagae, Lefèbvre, Cook, DeRose, Drettakis, Ebert, Lewis, Perlin & Zwicker (2010)**, *A survey
  of procedural noise functions*, CGF 29(8), `10.1111/j.1467-8659.2010.01827.x` (corpus; §3.2.1
  wavelet noise, §5.2.1 frequency clamping, §5 on sampling a noise on a surface).
- **Cook & DeRose (2005)**, *Wavelet noise*, ACM TOG 24(3), `10.1145/1073204.1073264` — read
  through the survey's account; the PDF at `graphics.pixar.com` returned HTML and the ACM copy
  is paywalled. **Not indexed.**
- **Lagae, Vangorp, Lenaerts & Dutré (2010)**, *Procedural isotropic stochastic textures by
  example*, Computers & Graphics 34(3), `10.1016/j.cag.2010.05.004` — downloaded to
  `/tmp/research/pdf/lagae2010.pdf` (9.5 MB) and read with `pdftotext -layout` (715 lines).
  **Not indexed.**
- **Weiß, Bayer & Westermann (2020)**, *Triplanar displacement mapping for terrain rendering*,
  Eurographics 2020, `10.2312/egs.20201016` — the preprint downloaded to
  `/tmp/research/pdf/weiss2020.pdf` and read with `pdftotext -layout` (295 lines). **Not
  indexed.**
- **Andersson (2007)**, *Terrain rendering in Frostbite using procedural shader splatting*,
  SIGGRAPH 2007 *Advances* ch. 5 (corpus `10.1145_1281500.1281668`) — the Overlay blend and its
  "does not affect areas where the base procedural mask is 0.0 or 1.0".
- **Söderlund (2015)**, *Procedural Modeling of Rocks*, Uppsala University MSc thesis — the record
  and the abstract were read; the PDF did not download. **Not obtained.**

### The critique and the verdicts (`/tmp/i29/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The surface-frame projection put a herringbone moiré over every face (the normal comes from the terrain's 2 m cell gradient, so the frame rotates per pixel and the noise's lattice swims in it); the Worley fractures were a giraffe's hide; the rock was one flat colour; the plain was one colour | FAIL |
| 2 | The 3D noise at the 3D point removed the weave, but the beds on the world's height made the massif a topographic map, and a fine corduroy came back at 700 m | NOT YET |
| 3 | The tilted beds and the split fixed the corduroy, but the rock was too light, the pink patches covered 16.75 % of the 2 m face, and the grass had an orange rim | NOT YET |
| 4 | The rock's grain was too contrasty (sandpaper), the saturation 7.6, and the scree was missing | NOT YET |
| 5 | The grain's contrast down, the relief up, the rock's hue in, the scree in: the face is stone, the massif is grey stone with scree, the plain has patches, the pink and the orange are gone | **GREAT** (the scree and the grass's own colour are NOT YET — see below) |

| Feature | Verdict |
|---|---|
| The rock's face (the weave) | **GREAT** |
| The mountain from 700 m | **GREAT** |
| The plain at 6 m | **GREAT** |
| The hills' ground at 6 m | **GREAT** |
| The scree at the grass's foot | NOT YET (a graded band, not a scatter) |
| The grass's own colour | NOT YET (a pure green with no dryness — not this change's business) |

**The change as a whole: GREAT.** The two blockers — the weave and the rock with no material —
are fixed, the colour defects are gone, and the cost went *down*.

### Measurements

- **The weave.** The structure tensor's coherence of the near band, and its direction:

  | | 2 m | 6 m |
  |---|---|---|
  | before | 0.709 at 43.3° | 0.655 at 45.2° |
  | after | 0.580 at 33.8° | 0.675 at 33.0° |

  The before's two bands shared a *world* direction (43.3° and 45.2°) — the signature of a
  lattice. The after's do not (33.8° and 33.0°), and the remaining anisotropy is the terrain's
  own relief: a 9 px high-pass of the face keeps the coherence at 0.676, and the *baseline* has
  the same at 0.625, because the terrain's 2 m cells' normals are anisotropic on a slope. The
  note's target of a coherence below 0.45 was therefore wrong — it asked the whole band to be
  isotropic when the terrain's relief cannot be — and is amended in the note with the
  measurement that replaces it.
- **The colour defects.** Pink (a warm brown mixed with a cool grey) and orange (a warm brown
  ring round the grass): 16.75 % and 9.67 % of the mountain's 2 m and 6 m faces before, **0.00 %
  and 0.00 %** after; the orange rim from 1415 pixels to none; the hills' pink from 0.32 % to
  0.04 %.
- **The plain.** The 60 px block standard deviation 2.83 → 3.34 DN, the mean within 0.02 DN.
- **The flat ground is unchanged.** The near-field standard deviation 6.11 → 5.86 DN, the mean
  133.66 → 133.31 DN.
- **Cost.** `bench_elements`, High, 1920 × 1080, `mountains`, the minimum over the
  diagnostic history, two runs of each build alternating in one session, the GPU at
  1695–1725 MHz: **`main_opaque_pass_3d` 0.136 ms on the baseline build, 0.127 ms on this
  one** — the mask's fBm went from four octaves to one and that pays for the rock's material.
  The GPU was contended for part of the session (vLLM at 94 %); the llama-swap models were
  evicted with the `gpu-tenant` lease's own recipe and the numbers are from the clean runs.
- **The full gate.** `cargo test` 252, clippy, doc, wasm, the GPU integration suite (22 files),
  the lib GPU modules clouds (10), terrain (8), cover (8) and dust (6), and `the_horizon_sky`
  alone: all pass.

### The next five items, ranked

1. **The scree is a graded band, not a scatter** — the grass meets the stone in a smooth wash
   where it should be stones of many sizes with turf between them. The idea and the place are
   right; the scatter is not there. The natural next piece of work on this material, and the
   rock's grain needs a *cell* structure for it (a Worley network of *stones*, not of walls).
2. **The grass has no dryness.** A pure green (RGB 120, 147, 92, saturation 55) with no dry
   grass, no litter and no yellowing at the edges. It is the crate's grass albedo and this
   change did not touch it, but against the rock's grey it reads as a colour change rather than
   as a place, and a meadow at a stone's foot wants some dryness in it.
3. **The isometric town at night is black** (22 h, Rain: mean 1 DN), carried three iterations.
4. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain` leak
   at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.
5. **A 25 mm/h storm hides the whole town under an orthographic camera**, carried. The rain's
   *readable* look is also less legible than the physical one (measured in the last iteration:
   the readable streaks carry 32 DN of contrast against the grass where the physical ones carry
   44), which is the defect the town capture keeps showing.

## Iteration 30 — 2026-10-20 — the ground has more than one colour: a meadow is not one green

**Commits:** `7197ba7` (the research note `docs/research/ground-colour.md`), `2251ce9` (the
palette), `1e6d030` (the shared fields), `a5c89f4` (the convex shares), `7775900` (the clump's
weight), `548e3c7` (the broken edge), `811adc6` (the square, not a `pow`), `6a9b3b9` (the docs,
the changelog and the showcase image), this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | The ground is **one colour** over the whole frame: the hue's spread over a near-ground patch is 0.010 on the plains and 0.016 on the hills | 2 (a beauty gap) | **this iteration** |
| 2 | The grass/rock boundary is a blurred paint edge | 1 | **this iteration** (the same change, at the boundary) |
| 3 | The scree is a graded band, not a scatter | 2 | carried |
| 4 | The isometric town at night is black (22 h, Rain: mean 1 DN) | 1 | carried |
| 5 | The overcast dome is flat (1.08:1 against CIE's 2:1–3:1) | 1 | carried |

Rank 1 beat the carried items because it is in *every* outdoor frame of the game: a third-person
camera looks at the ground from one to twenty metres most of the time. The crate's ground
already had the *shape* of a meadow — its silhouette, its tufts, its patches of bare earth — and
none of the *colour*.

### What changed

- **The turf is a palette of four tones** — fresh grass, the yellow of a dry patch, the straw
  of a bleached one, the earth of a worn place — mixed by a **score**, exactly Papavasiliou's
  model: a low-frequency field decides how much of each tone is *around* and a finer one
  decides which tone this piece of ground *wears*, and the score is their product.
- **The four tones are mixed as *shares* that sum to one.** The mixture is a convex combination
  of them, so the ground's mean colour and brightness cannot drift. Three other normalisations
  were tried and each failed (the unweighted mean of the tones drifted the mean brightness
  3.2 DN; normalising by the mixture's own colour cancelled the hue entirely; a per-channel
  mean cancelled it again).
- **The palette's two fields are the ground's own** — the large-scale field the layer variation
  already uses, and the turf's coarse and fine detail fields — so the palette's only new noise
  evaluation is its selection field at five metres. It is also a *better* field than the
  palette's own: the ground's varies more, and the hue's spread over a near-ground patch is
  0.0391 with the shared field against 0.0173 with the palette's own.

### Sources

- **Papavasiliou, N. (2015)**, *Real-Time Grass (and Other Procedural Objects) on Terrain*,
  JCGT 4(1):26–49 (`https://jcgt.org/published/0004/01/02/paper.pdf`, downloaded to
  `/tmp/research/pdf/papavasiliou2015.pdf` and read with `pdftotext -layout`, 1211 lines; no DOI).
  **§3.3, "Controlling Plant Distribution", is the model this iteration adopts**: "Vegetation in
  nature does not always uniformly cover the ground. The density usually varies due to
  variations in the underlying soil and parts of the terrain may be altogether bare."; the
  **reference colour** and the **score** — for each terrain type a reference colour, and at each
  point a score for that type "based on the inverse distance of the terrain type's reference
  color to the terrain's base map color at that point", in HSV; the **weight vector** `w`, which
  "allows us to ignore certain color components during the calculation of the distance … where a
  reasonable choice is gray for which hue is meaningless", and the note that it "is also useful
  in many cases to discard the lightness component, as its variation over the terrain is often
  the result of shadowing effects, which are unrelated to terrain type"; the **exponent** `p`,
  "useful in controlling the sharpness of the transition between different terrain types. By
  using lower values for p, broader transitional regions between terrain types can be achieved
  while larger values create sharper, more defined transitions"; and the **threshold**. **Not
  indexed.**
- **Lefèbvre, S., Neyret, F. (2003)**, *Pattern based procedural textures*, ACM SIGGRAPH
  Symposium on Interactive 3D Graphics (I3D '03):203–212, DOI `10.1145/641480.641518`
  (downloaded to `/tmp/research/pdf/lefebvre2003.pdf` and read with `pdftotext -layout`, 968
  lines). Used: "an algorithm which procedurally determines the texture value at any surface
  location by **aperiodically combining provided patterns** according to user-defined controls
  such as a probability distribution (possibly non stationary)"; the **probability map**, "an
  explicit indirection map … simply contains the indices of the patterns to be used"; and the
  **dithering operator** for the areas map's interpolation, which this iteration replaces with a
  smooth boundary so the transition is not a speckle. **Not indexed.**
- **Andersson, J. (2007)**, *Terrain rendering in Frostbite using procedural shader splatting*,
  SIGGRAPH 2007 *Advances* ch. 5 (corpus `10.1145_1281500.1281668`): the "bland transitions" the
  crate inherited and the per-material detail that fixes them.
- **Grenier, C., Guérin, É., Galin, É., Sauvage, B. (2024)**, *Real-time Terrain Enhancement with
  Controlled Procedural Patterns*, CGF 43(1):e14992, DOI `10.1111/cgf.14992` (the abstract and
  the search index's account; the PDF is behind Wiley's 403): the **palette of control maps** for
  artist control over a structured noise's patterns, and the principle that a terrain's details
  "correspond to the underlying terrain characteristics". **Not indexed.**
- **Lagae et al. (2010)**, *A survey of procedural noise functions*, CGF 29(8),
  `10.1111/j.1467-8659.2010.01827.x` (corpus), as in `ground-detail.md`.
- **`ground-detail.md`** (iteration 28) and **`rock-material.md`** (iteration 29): the ground's
  two detail scales, the layer masks, the layer variation, and the rock's own material.

### The critique and the verdicts (`/tmp/i30/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The carry term used the base colour's *absolute* chroma, and a dark albedo has a small one by construction, so the palette reached the ground at 39 % of its strength and the hue's spread *fell*; the near field had no variation; the far field was a haze | NOT YET |
| 2 | The hue's spread was four times better with the shared fields, but the mean drifted 3.2 DN and three `pow` calls cost 84 % of the opaque pass | NOT YET |
| 3 | The convex shares hold the mean, the square is a multiply, and the clump's coarse field breaks the patch's edge: the plains is a meadow | **GREAT** |

| Feature | Verdict |
|---|---|
| The plain at 6 m | **GREAT** |
| The hills' ground at 6 m and 2 m | **GREAT** |
| A game's own ground (`GroundExtension`) | **GREAT** |
| The rock (unchanged, byte-identical) | **GREAT** |
| The dry patch's own *texture* | NOT YET (a tint on green blades, not straw-coloured blades) |
| The patches' texture against the green grass's | NOT YET (a dry patch is coarser) |

**The change as a whole: GREAT.** The blocker — a ground that is one colour — is fixed, the mean
does not move, the rock is untouched, and the cost is 5 %.

### Measurements

- **The ground is not one colour**: the G/R ratio's spread over a 580 × 240 px patch of the near
  ground — the plains 0.0097 → **0.0226**, the hills at 6 m 0.0258 → **0.0339**, the hills at 2 m
  0.0156 → 0.0169, the dunes 0.0047 → 0.0096. The far field's *local* spread (a 25 px blur
  subtracted) 0.0098 → 0.0124.
- **The mean does not move**: the plains' mean brightness 135.69 → 135.55, the hills at 2 m
  131.50 → 132.14, and the mean hue within 0.002. The shares are a convex combination, so this
  is a property of the construction and not of the tuning.
- **The palette's own span**: the G/R ratio runs from 1.00 (the fresh tone, the identity) to 0.86
  (the straw).
- **The rock is byte-identical**: the 6 m face reads 136.5, 130.7, 124.8 before and after, and
  the pink and the orange are 0.00 % in both.
- **The turf is untouched**: the flat ground's near-field standard deviation 6.11 → 5.86 DN.
- **Cost.** `bench_elements`, High, 1920 × 1080, `mountains`, the minimum over the diagnostic
  history, one build at a time with the GPU quiet before each run and the clocks logged in the
  same shell, at 1800 MHz: **`main_opaque_pass_3d` 0.137 ms on the baseline build, 0.144 ms on
  this one** — a 5 % regression. The palette's three `pow` calls were 84 % of it (0.252 ms), and
  writing the square as a multiply brought it back.
- **The full gate.** `cargo test` 253, clippy, doc, wasm, the GPU integration suite (22 files),
  the lib GPU modules clouds (10), terrain (8), cover (8) and dust (6), and `the_horizon_sky`
  alone: all pass.

### The WESL traps this iteration cost an hour on

All three make the surface render as a **dark navy with no shader error at all**, and all three
are now in `AGENTS.md`:

- A name used from another WESL module must be in the **import list**. An unimported name makes
  naga emit a broken shader *silently*.
- **`pow(x, p)` with `p` in a `let`** does the same; a literal exponent works.
- A local that **shadows** another local in the same scope does the same.

### The next five items, ranked

1. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too — the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate and is the natural
   next step; the geometry half (a grass mesh) belongs to `bevy_flora`.
2. **The scree is a graded band, not a scatter** — stones of many sizes with turf between them.
   The idea and the place are right; the scatter is not there. The rock's grain needs a *cell*
   structure for it (a Worley network of *stones*, not of walls).
3. **The isometric town at night is black** (22 h, Rain: mean 1 DN), carried three iterations.
4. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain` leak
   at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.
5. **A 25 mm/h storm hides the whole town under an orthographic camera**, carried. The rain's
   *readable* look is also less legible than the physical one (measured in iteration 28: the
   readable streaks carry 32 DN of contrast against the grass where the physical ones carry 44),
   which is the defect the town capture keeps showing.

## Iteration 31 — 2026-10-20 — the night sky lights the ground: a moonless night is not a black screen

**Commits:** `2e89e8e` (the research note `docs/research/night-sky-light.md`), `8c896cb` (the
implementation), `6d316c3` (the docs, the changelog and the showcase image), this log. **Sibling
repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | A **moonless night is a black screen**: the isometric town at 20 h renders at 1 DN | 1 (physically wrong) | **this iteration** |
| 2 | The town's ground is a flat olive-brown at 18 h | 2 | carried (the example's own material) |
| 3 | The scree is a graded band, not a scatter | 2 | carried |
| 4 | The dry patch's own texture (a tint on green blades) | 2 | carried |
| 5 | The overcast dome is flat | 1 | carried (blocker diagnosed in iteration 28) |

Rank 1 beat the carried items because it is the only one that is *class 1* — a physically
wrong result rather than a beauty gap — and because it makes the game unplayable at night: the
player cannot see their character, their ground, or their world. It has also been carried three
iterations, so it is the most persistent defect in the crate's backlog.

### What changed

- **The night sky's radiance is part of the ambient.** The airglow, zodiacal light and starlight
  that lie *behind* the atmosphere are the crate's `ILLUMINANCE_FLOOR_LUX = 0.0005 lx`, and the
  crate used it for the exposure meter's floor and the background's clear colour — but the
  **ambient that illuminates surfaces** came from `cloud_ambient`, which is zero when neither
  the sun nor the moon is up. So a moonless night had no ambient at all: the sky was lit and the
  world on it was not.
- **The sky probe's cube carries it too.** A camera *with* a probe has its ambient set to 0 —
  the light comes from the cube — and the probe's cube is *display-referred*, so the radiance is
  converted with the camera's exposure, as the fog's haze is.
- **The colour is the *scotopic* one** — a cool blue-green, from Jensen et al. 2001's blue shift
  and the rod's curve, whose peak is at 507 nm.
- **The cloud does not dim it**: the night sky is behind the atmosphere, and the floor is
  *added* to the bodies' light, not multiplied with it.

### Sources

- **Jensen, H. W., Durand, F., Stark, M. M., Premože, S., Dorsey, J., Shirley, P. (2001)**,
  *A Physically-Based Night Sky Model*, ACM SIGGRAPH 2001, DOI `10.1145/383259.383306` (**in the
  corpus**, stem `10.1145_383259.383306`, read in full for this note). Used: Figure 2, the
  sources of natural illumination at night (zodiacal $1.2\times10^{-7}$, integrated starlight
  $3.0\times10^{-8}$, airglow $5.1\times10^{-8}$, galactic $9.1\times10^{-9}$, cosmic
  $9.1\times10^{-10}$ W/m², the full moon $2.1\times10^{-3}$); §6, the four sources of diffuse
  night light and the sentence that is the reason for this note — "These effects are especially
  important on moonless nights, when **a small change in illumination can determine whether an
  object in the scene is visible or invisible**"; §6.2, "Airglow is the principal source of
  light in the night sky on moonless nights"; §7, "**The sky is never completely black**" and the
  blue shift. **Not indexed** (the ONNX out-of-memory error in `corpus.md`).
- **The crate's own `twilight.md`**: the *What great looks like* the note had already set — "a
  moonless sky is near-black with stars and **the horizon's silhouettes still readable against
  it**" — and its item 8, the visual target of "silhouettes against a moonless sky", which the
  *sky* delivered and the *ground* did not.
- **The crate's own `celestial.md`**: Jensen's numbers as the crate uses them, and the
  non-lunar night illumination's sum.

### The critique and the verdicts (`/tmp/i31/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The fix was in the *ambient*, which is right for a camera without a probe and invisible for one with it: the town has a probe, so it was still black (0.907 → 0.9085 DN). And the cube is display-referred: adding the raw radiance put a value 10⁵ times too small in it (the mean went 0.91 → 2.15 DN, the houses appeared and the ground did not) | NOT YET |
| 2 | The night sky's radiance in the ambient *and* in the probe's cube, display-referred: the town is a night scene, the day is byte-identical, and the cost is nil | **GREAT** |

| Feature | Verdict |
|---|---|
| A moonless night, isometric | **GREAT** |
| A moonless night, `day_night` (a followed camera with a probe) | **GREAT** |
| The twilight's continuity | **GREAT** |
| The day | **GREAT** (byte-identical) |
| The cost | **GREAT** (0.8 %) |
| The night sky's *colour* | NOT YET (brown; pre-existing, and the next item) |
| The town's ground | NOT YET (the example's own flat plane) |

**The change as a whole: GREAT.** The blocker — a moonless night is a black screen, and the
player cannot see their character — is fixed, on both the probe's path and the ambient's path,
the day is untouched, and the cost is nil.

### Measurements

- **A moonless night is not black**: the isometric town in `Clear`, `ELEMENTS_ZOOM=60
  ELEMENTS_PITCH=35`, 2026-04-15 — 20 h **0.907 → 24.763 DN** (27×) and 22 h **0.739 → 22.443
  DN** (30×), with the blue/red ratio rising from 0.992 to 1.059.
- **Both paths**: the `day_night` example (a followed camera, with a probe) at 22 h
  **3.409 → 14.230 DN**, and the `terrain_tour` camera (no probe) at 22 h 20.9 → 29.2 DN.
- **The day is untouched**: the town at 12 h reads 148.208 DN before and after, and at 18 h
  108.565 DN before and after — byte-identical.
- **The twilight has no step**: the terrain's mean luminance at 17, 18, 19, 19.5, 20, 21 and
  22 h is 159.5, 150.7, 118.0, 80.9, 65.8, 22.7 and 29.2 — the sun's fall and the night sky's
  floor meet, and the night *rises* as the twilight ends, as it must.
- **The moon still wins**: a moonlit night is four times brighter than the moonless one on the
  same frames, against Jensen's Figure 2: the full moon is 8700 times the moonless sources.
- **Cost.** `bench_elements`, High, 1920 × 1080, `mountains`, the minimum over the diagnostic
  history, the two builds run back to back in one session with the clocks logged throughout:
  **`main_opaque_pass_3d` 0.251 ms on the baseline build, 0.253 ms on this one** — 0.8 %, and the
  fix is on the CPU. (The GPU was contended for part of the session; the llama-swap models were
  evicted with the `gpu-tenant` lease's own recipe and the numbers are from the clean runs, at
  the same clocks for both builds.)
- **The full gate.** `cargo test` 253, clippy, doc, wasm, the GPU integration suite (22 files),
  the lib GPU modules clouds (10), terrain (8), cover (8) and dust (6), and `the_horizon_sky`
  alone: all pass.

### A test that asserted the bug

`celestial::tests::the_ambient_carries_the_diffuse_share_and_nothing_at_night` asserted
`brightness == 0.0` for a moonless night, with the comment "a night with no moon stays dark
whatever the cloud". That was the crate's *intent*, written down and tested — and it is what
made the night a black screen. The test now asserts the correction, and it is named
`..._and_the_night_sky_at_night`.

### The next five items, ranked

1. **The night sky is brown.** The `day_night` example's sky at 22 h reads R/G = 1.542, and
   `twilight.md`'s *What great looks like* says the night should be "**Nothing brown**". It is
   pre-existing (the baseline reads 1.599) and unchanged by this iteration, but it is the same
   night and it is the crate's own note that says it is wrong.
2. **A game at night has nothing to light with.** A moonless night is *shadowless* — physics —
   but a game wants a torch, a street lamp, a window, and the crate gives a game nothing to
   place them with: no `Environment` query for the local light, no hook for a game's own lights.
   That is an API gap, and it is in the crate's class-3 list.
3. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too — the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
4. **The scree is a graded band, not a scatter** — stones of many sizes with turf between them.
   The idea and the place are right; the scatter is not there.
5. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain` leak
   at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.

## Iteration 32 — 2026-10-20 — the night sky is blue, not brown: what is behind the atmosphere

**Commits:** `4d79c3f` (the research note `docs/research/night-sky-colour.md`), `b19fd3d` (the
implementation), `870461a` (the test), `9dec61b` (the docs, the changelog and the showcase
image), this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | The **night sky is sepia**: at 22 h with no moon its red/green ratio is 1.52, and the crate's own `twilight.md` says "Nothing brown" | 1 (physically wrong) | **this iteration** |
| 2 | A game at night has nothing to light with (no `Environment` query, no hook for a game's own lights) | 3 (API) | carried |
| 3 | The dry patch's own texture (a tint on green blades) | 2 | carried |
| 4 | The scree is a graded band, not a scatter | 2 | carried |
| 5 | The overcast dome is flat | 1 | carried (blocker diagnosed in iteration 28) |

Rank 1 beat the carried items because it is *class 1* — a physically wrong result, and the
crate's **own research note** says the night must be blue, so the crate contradicts itself in the
one domain where it is most specific. It is also in every night frame of the game.

### What changed

- **The clear colour carries the *scotopic* colour divided by the atmosphere's transmittance.**
  The clear colour is what lies *behind* the atmosphere, and the atmosphere is drawn over it with
  its transmittance — measured at (0.904, 0.640, 0.353) at the zenith, which is Rayleigh
  extinction and is *warm*. A neutral clear colour behind a warm-extinguishing atmosphere is a
  brown sky; the fix is to make the clear colour blue enough that what comes through is the
  cool blue-black the rod's eye expects.

### Sources

- **Jensen, H. W., Durand, F., Stark, M. M., Premože, S., Dorsey, J., Shirley, P. (2001)**,
  *A Physically-Based Night Sky Model*, ACM SIGGRAPH 2001, DOI `10.1145/383259.383306` (**in the
  corpus**, stem `10.1145_383259.383306`, read in full). Used: §6.2, "Airglow is the principal
  source of light in the night sky on moonless nights"; §6, the four sources of diffuse night
  light and the sentence that is the reason — "These effects are especially important on
  moonless nights, when a small change in illumination can determine whether an object in the
  scene is visible or invisible"; §7, "**The sky is never completely black**" and the blue shift.
  **Not indexed** (the ONNX out-of-memory error in `corpus.md`).
- **The crate's own `twilight.md`**: the *What great looks like* — "a moonless sky is near-black
  with stars and the horizon's silhouettes still readable against it… **Nothing brown**" — and its
  Model 1, which puts the night sky's radiance behind the atmosphere and lets the transmittance
  do the rest.
- **The crate's own `celestial.md`** and `night-sky-light.md`: the night sky's radiance and the
  *scotopic* colour that iteration 31 put in the *ambient*.
- **The CIE 1951 scotopic luminous efficiency function**, through Jensen §7's blue shift: the
  rod's curve peaks at 507 nm and falls away on both sides, so the airglow's green (557.7 nm) and
  red (630 nm) lines are read as a cool blue-green.

### The critique and the verdicts (`/tmp/i32/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The scotopic colour put *directly* into the clear colour left the sky brown (the zenith's red/green 1.411 → 1.415): the clear colour is behind the atmosphere, and the atmosphere's transmittance is warm, so a blue clear colour comes through less blue. The division is the whole fix | NOT YET |
| 2 | The scotopic colour divided by the measured transmittance: the zenith's red/green 0.783 and blue/green 1.197, the horizon still warmer, the probe's path bluer too, the day and the twilight byte-identical, and the cost nil | **GREAT** |

| Feature | Verdict |
|---|---|
| The night sky's colour | **GREAT** |
| The sky's gradient toward the horizon | **GREAT** |
| The sky probe's path | **GREAT** |
| The day and the twilight | **GREAT** (byte-identical) |
| The cost | **GREAT** (0 %) |
| The zenith's faint warm cast at the very top | NOT YET (cosmetic; the status band's glow) |
| A moonless night's shadows | NOT YET (physics; a game needs its own lights) |

**The change as a whole: GREAT.** The blocker — a sepia night sky, which the crate's own note
says is wrong — is fixed on both paths, the day is untouched, and the cost is nil.

### Measurements

- **The sky is blue.** The `day_night` example at 22 h (2026-07-14, `Clear`, the sun at −17.5° and
  the moon at −14.2°, so neither is up), the zenith's red/green **1.411 → 0.783** and its
  blue/green **0.551 → 1.197**; 30° up 1.742 → 0.993; the low sky 2.119 → 1.626, so the
  horizon is still warmer than the zenith, as the longer path says.
- **Both paths.** The `weather_tour`'s upper sky, which comes from the sky probe's cube rather
  than the clear colour, goes from blue/green 1.179 to **1.266**.
- **The day and the twilight are byte-identical**: the dusk's sky 209.5, 208.5, 212.0 and the
  dawn's 223.8, 206.7, 202.6, before and after.
- **Cost.** `bench_elements`, High, 1920 × 1080, `mountains`, the minimum over the diagnostic
  history, the two builds run back to back in one session with the clocks logged throughout:
  **`main_opaque_pass_3d` 0.147 ms on the baseline build, 0.147 ms on this one** — the fix is one
  `vec3` written per camera per frame.
- **The full gate.** `cargo test` 254, clippy, doc, wasm, the GPU integration suite (22 files),
  the lib GPU modules clouds (10), terrain (8), cover (8) and dust (6), and `the_horizon_sky`
  alone: all pass. (Two cloud tests failed on a first run and passed on the second: the GPU was
  at 100 % with another tenant's model resident, and the `gpu-tenant` lease's eviction recipe
  cleared it. The same two failed on the *baseline* build on that run.)

### The next five items, ranked

1. **A game at night has nothing to light with.** A moonless night is *shadowless* — physics —
   but a game wants a torch, a street lamp, a window, and the crate gives a game nothing to
   place them with: no `Environment` query for the local light, no hook for a game's own lights.
   That is an API gap, and it is the crate's class-3 list.
2. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too — the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
3. **The scree is a graded band, not a scatter** — stones of many sizes with turf between them.
   The idea and the place are right; the scatter is not there.
4. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain` leak
   at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.
5. **A 25 mm/h storm hides the whole town under an orthographic camera**, carried. The rain's
   *readable* look is also less legible than the physical one (measured in iteration 28: the
   readable streaks carry 32 DN of contrast against the grass where the physical ones carry
   44), which is the defect the town capture keeps showing.

## Iteration 33 — 2026-10-20 — a game lights its own night: the exposure, and what a light has to be to read

**Commits:** `4fb14ea` (the research note `docs/research/game-lights.md`), `e9d4583` (the
implementation with its tests), `6539053` (the `isometric` example), `a430941` (the README, the
changelog and the note's Deviations), `c9d0d37` (the lights off in daylight), this log.
**Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **A game at night has nothing to light with**: the crate meters an exposure and gives a game no way to balance its own light against it | 3 (API) | **this iteration** |
| 2 | The scree is a graded band, not a scatter (stones of many sizes with turf between them) | 2 | carried |
| 3 | The dry patch's own texture (a tint on green blades) | 2 | carried |
| 4 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |
| 5 | A 25 mm/h storm hides the whole town under an orthographic camera | 2 | carried |

Rank 1 beat the carried items because it is the one that makes a *game* of the crate work: the
crate simulates a day and a night, a sun and a moon, a sky and a metered exposure, and a game
that puts a character in that world at night has **nothing to light them with**. It is also the
crate's own class-3 list, and the crate's `night-sky-light.md` already names it: "a game at
night wants its own lights (a torch, a street lamp, a window), and the crate gives a game
nothing to place them with". The carried items are a beauty gain on a detail at the boundary
of two surfaces, against an API gap that stops a game shipping its night.

### What changed

- **`ElementsExposure`**, a resource holding the camera's EV100 and its exposure factor,
  refreshed every frame. The celestial plugin writes the *followed* camera's exposure there,
  whether the crate metered it or the game drove it (`ElementsCamera::auto_exposure` off).
- **`Environment::exposure()`, `display_value(luminance)`, `luminance_for_display(display)`,
  `light_intensity(luminance, distance, albedo)`, `light_illuminance(intensity, distance)` and
  `night_light_colour()`**, and the same functions in the new `light` module, flat from the
  crate's root.
- **The lightning's own `display_value` is now that module's**, so there is one path for the
  feature and no duplicate constant.

**The shape of it.** A scene-linear value of **0.18 is 18 % grey, the middle of the picture**,
and the crate's metering is built so that an 18 % grey surface reads at 0.18 *at every hour* —
because the exposure is applied inside Bevy's PBR shader and the tonemapper after it, so the
number the crate names is the tonemapper's *input*. The power a light needs scales
*inversely* with the exposure: the same lamp wants **62 253 lm at noon and 2.8 lm at
midnight** for the same picture, so a game that hard-codes a lamp's lumens is wrong by five
orders of magnitude between the two, and a game that asks the crate is never wrong.

### Sources

- **Patry, J. (2021)**, *Real-time samurai cinema*, SIGGRAPH *Advances* (corpus
  `s2021-advances-pdf-66-mb`, the "Tonemapping | Exposure" section, lines 2990–3060, and the
  low-light vision section, lines 3855–3900). Used: "Lights are authored using **physical
  units**"; "we also switched to a **primarily illuminance-based exposure system**, which takes
  albedo out of the equation altogether", with luminance used "only if the highlight luminance
  exceeds a threshold" — "fire particles, bright specular highlights"; the *feedback loop* they
  had before ("the feedback loop between luminance-based exposure and albedo… resulted in
  albedo values that were too dark"), which the crate does not have, and which is why **a game's
  own light does not move the camera's exposure** — the stable behaviour, and the reason the
  gap is not an exposure bug; and the low-light section's **Purkinje shift** ("low-light vision
  is not 'black & white'… a color shift introduced by the rods as they take over"), which is
  why a game's light at night should be the crate's *scotopic* colour and not a warm one.
  **Not indexed** (the ONNX out-of-memory error in `corpus.md`).
- **Bevy 0.20's source** (`$R` = `~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f`):
  `$R/bevy_camera-0.20.0/src/camera.rs:275` — `Exposure::exposure()` is `2^(−ev100) / 1.2`;
  `$R/bevy_pbr-0.20.0/src/render/light.rs:574` and `:711` — a light's power is **luminous power
  in lumens**, mapped to `Φ / 4π` for a point light *and* for a spot light ("we choose to use
  the same 4\*PI divisor in both cases so that toggling between point light and spot light
  keeps lit areas lit equally"), where Filament would use `π` for a spot; `$R/bevy_pbr-0.20.0/
  src/render/pbr_lighting.wesl` — `getDistanceAttenuation` (`1/d²`) and the spot's
  `spot_scale`/`spot_offset` (the cone's attenuation is 1 inside the inner angle and 0 at the
  outer); `$R/bevy_pbr-0.20.0/src/render/pbr_functions.wesl:850` — the exposure is applied
  *inside* the PBR shader, before the tonemapper; `$R/bevy_light-0.20.0/src/spot_light.rs:152` —
  Bevy's own calibration, "1,000,000 lumens is a very large 'cinema light' capable of
  registering brightly at Bevy's default 'very overcast day' exposure level".
- **The crate's own `celestial.md`** — the metering model (`ev100_for`, `exposure_for_adaptation`,
  `scene_illuminance`) — and **`night-sky-light.md`** — the illuminance floor and the *scotopic*
  colour.
- **Jensen et al. 2001** (through the crate's own `night-sky-colour.md`) — the blue shift and
  the scotopic curve.

### The critique and the verdicts (`/tmp/i33/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The API was right and the *example* was not: the lamp's pool was a 1 m blob, the lamp's head blew out to 255 DN (it was lit by its own light 0.15 m away), and the paving's 10 m slab cut the pool with a hard edge | NOT YET |
| 2 | A `SpotLight` for the hand-held light, the head *emissive* rather than lit, the lamp at the square's centre and the paving at 14 m: the frame's contrast **2.44 → 13.97 DN**, the head not blown, the day untouched | NOT YET |
| 3 | The head's radiance as a scene-linear value (8.0, a step above the pool's 0.30) and the lamp's light off in daylight: the contrast **14.18 DN**, the day's maximum 255 → 232 DN, the noon capture's mean within 0.5 DN of the baseline's | **GREAT** |

| Feature | Verdict |
|---|---|
| The exposure a game reads | **GREAT** |
| The light's power, in lumens | **GREAT** |
| The night colour | **GREAT** |
| The town's night, at full frame | **GREAT** (2.44 → 14.18 DN) |
| The town's night, at 350 % | **GREAT** (the head, the falloff on the walls, the four shadows) |
| The day | **GREAT** (within 0.5 DN of the baseline) |
| The cost | **GREAT** (no GPU work at all) |
| The lamp's head is a cube | NOT YET (cosmetic; the example's set dressing) |
| The roofs' near-black tops | NOT YET (physics; the lamp is under the roof line) |

**The change as a whole: GREAT.** The API gap is closed in one path, the units are checked
against Bevy's own calibration, the promise is proved in a test that runs outside the crate, and
the demonstration in the example takes the town's night from a flat wash to a lit place.

### Measurements

- **The night reads.** The isometric town at 22 h (2026-04-15, `Clear`, zoom 45 m, pitch 35°):
  the frame's standard deviation **2.44 → 14.18 DN**, its maximum 49.0 → 176.7 DN, and the
  four houses' shadows now read.
- **The same light reads the same at every hour.** The scene-linear value the pool reads at is
  **0.06000 at 12 h, 22 h and 2 h** — to five figures — while the *picture* differs, because
  the tonemapper compresses the daylight scene, which is right: a torch does not read at noon.
- **The day is untouched.** The noon capture's mean is (143.12, 151.39, 130.84) against the
  baseline's (142.62, 150.86, 130.20) — 0.5 DN in each channel, within the GPU's
  non-determinism — and the standard deviation within 0.03.
- **The metering, measured through the crate's own `ev100_for`:** the moonless night
  (0.0005 lx) is EV100 −4.75 and the noon sun (10 000 lx) EV100 9.75; an 18 % grey surface reads
  at 0.18 at both.
- **Cost.** `bench_elements`, High, 1920 × 1080, the minimum over the diagnostic history, the
  two builds run **back to back in one session** so the card's load is the same for both:
  round 1 (the card idle) the baseline's total **1.901 ms** and this one's **1.925 ms**; round
  2 (the card loaded by another tenant) the baseline's **11.294 ms** and this one's
  **11.188 ms**. Every pass is within the noise (`elements_clouds_march` 0.561 → 0.549 and
  1.369 → 1.343; `elements_wind` 0.166 → 0.155 and 0.395 → 0.366; `main_opaque_pass_3d` 0.052
  → 0.052 and 0.207 → 0.204). The change is arithmetic on a number the crate already computes,
  so there is no new pass, no new buffer and no new binding.
- **The full gate.** `cargo test` 260 plus the 4 new public-API tests, clippy, doc, wasm, the
  GPU integration suite (22 files), the lib GPU modules clouds (10), terrain (8), cover (8) and
  dust (6), and `the_horizon_sky` alone: all pass.

### The next five items, ranked

1. **The scree is a graded band, not a scatter** — the grass meets the stone in a smooth wash
   where it should be stones of many sizes with turf between them. The idea and the place are
   right; the scatter is not there. The natural next piece of work on this material, and the
   rock's grain needs a *cell* structure for it (a Worley network of *stones*, not of walls).
2. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too — the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
3. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain` leak
   at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.
4. **A 25 mm/h storm hides the whole town under an orthographic camera**, carried. The rain's
   *readable* look is also less legible than the physical one (measured in iteration 28: the
   readable streaks carry 32 DN of contrast against the grass where the physical ones carry
   44), which is the defect the town capture keeps showing.
5. **A game's own light has no shadow of its own to fall on the crate's weather.** A lamp lights
   the town, but the crate's rain and snow are lit by the crate's lights alone, so a lamp in a
   downpour lights the *ground* and not the *rain*. A hook for a game's own lights in the
   precipitation and cloud passes would close it; the ownership map keeps the geometry in
   `bevy_flora`, but the lighting hook is this crate's.

## Iteration 34 — 2026-10-21 — the scree is a scatter of stones, not a band of mixed colour

**Commits:** `66a5fb8` (the research note `docs/research/scree.md`), `32f15b5` (the
implementation with its tests), `81b01c7` (the changelog and the note's Deviations), `c3bd339`
(the README and the showcase image), `9b74f20` (the scree's cost: two cell lookups and one
grain, not four and two), `23aae56` (the measured cost and the note's last two deviations),
this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The scree is a graded band, not a scatter** — the grass meets the stone in a smooth wash where it should be stones of many sizes with turf between them | 2 | **this iteration** |
| 2 | The dry patch's own texture (a tint on green blades) | 2 | carried |
| 3 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |
| 4 | A 25 mm/h storm hides the whole town under an orthographic camera | 2 | carried |
| 5 | A game's own light does not fall on the crate's weather | 3 | carried from iteration 33 |

Rank 1 beat the carried items because it is the *rock's* missing half, and the rock is the
crate's most prominent material. The crate has given the rock a material (strata, fractures, a
grain) and a place to put it, but the material is a *texture* where the world has *objects*: a
scree is the most recognisable place where the difference shows, and the log has named it
first for five iterations running. The overcast dome is class 1 and so ranked higher by the
class order, but its blocker is diagnosed and its fix is a *sky* change, and it has carried
four iterations with its measurements in `/tmp/i28`.

### What changed

- **The stones are the cells of a 2D cellular network.** The nearest feature point is a
  stone's centre and the distance to it is the stone's radius. The crate's own rock note
  records why that is the right structure — and why the *same* network cannot make the
  fractures: "A Worley cell network cannot make them — it bounds *cells*, so a face reads as a
  giraffe's hide". Used the other way round, as the **stones** rather than as the **cracks**,
  it is the missing piece.
- **Two networks, nested**: a coarse one for the big stones and a fine one for the small ones,
  the fine ones only where the coarse ones do not cover — a real talus's long tail.
- **Each stone has its own relief** (the distance field's gradient domes it, so the sun rakes
  its edge) and **its own contact shadow** on the matrix beside it, so the stones sit *in* the
  dirt.
- **The ground between the stones is the scree's own matrix**, not the turf: a scree has no
  grass in it, and a lawn with pebbles on it is a lawn.
- **The band is 16°–40°**, an apron rather than a line.
- `cell2` in `noise.wesl` and its CPU mirror in `gpu/noise_mirror.rs`.

### Sources

- **Verdian, J. P., Sklar, L. S., Riebe, C. S., Moore, J. R. (2021)**, *Sediment size on talus
  slopes correlates with fracture spacing on bedrock cliffs*, Earth Surface Dynamics 9:1073,
  DOI `10.5194/esurf-9-1073-2021` (open access; the PDF was read in full, 914 lines of
  `pdftotext -layout`). Used: "The detachment of rock fragments from fractured bedrock on
  hillslopes creates sediment with an initial size distribution that sets the upper limits on
  particle size for all subsequent stages in the evolution of sediment in landscapes"; the
  measurement — the finest talus clasts "as small as 2 mm, and median b-axis diameters ranging
  from 10 to 575 mm", and "Median fracture spacing and particle size vary by more than 10-fold
  and correlate strongly with lithology" — so a scree has stones of *many* sizes and its
  median moves between sites; and the weathering that erases the size distribution "in the
  critical zone", which is the turf growing over it. **Not indexed** (the ONNX out-of-memory
  error in `corpus.md`).
- **Dorsey, J. O'B., Edelman, A. S., Jensen, H. W., Legakis, J., Pedersen, H. K. (1999)**,
  *Modeling and rendering of weathered stone*, ACM SIGGRAPH 1999, DOI `10.1145/311535.311560`.
  The record was read; **the PDF was not obtained**. Named here as the canonical treatment of a
  weathered stone surface, and its *approach* — model the stone's weathering and render the
  result — is the one the crate already follows for the rock's own material.
- **The crate's own `rock-material.md`**: the strata, the fractures and the grain, and the
  sentence that is the key to this change — "A Worley cell network cannot make them — it
  bounds *cells*, so a face reads as a giraffe's hide"; and the corduroy lesson, which is why
  the scatter is band-limited by the cell.
- **Andersson, J. (2007)**, *Terrain rendering in Frostbite using procedural shader splatting*,
  SIGGRAPH 2007 *Advances* ch. 5 (corpus `10.1145_1281500.1281668`): the Overlay blend, which
  breaks the scree's band edges with the same `detail` field the layer boundaries use.

### The critique and the verdicts (`/tmp/i34/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The stones appeared, but as **dark holes in the grass**: the stone's colour multiplied the ground's albedo instead of replacing it, and the band sat on the *soil's* band rather than at the rock's foot | NOT YET |
| 2 | The stone's colour replaced the ground's, and the band moved to the rock's foot — but the stones' relief scaled with the cell, so the smallest stones' rims tilted by 85° and they rendered as black crescents | NOT YET |
| 3 | The relief is a dimensionless slope, the network is nested, and the matrix is the scree's own: the stones are countable at 3 m, the apron is wide, and the distance is calm | NOT YET |
| 4 | The matrix was still warm (it read as a dirt road tracing the hillside) and the stones cast no shadow: the matrix is greyed toward the rock and the stones gained a contact shadow | **GREAT** |

| Feature | Verdict |
|---|---|
| The stones, at 3 m | **GREAT** (countable, many sizes, each with a lit top and a shaded flank) |
| The stones' contact shadow | **GREAT** (they sit in the dirt) |
| The apron at 25 m | **GREAT** (a wide stony flank, the green thinning through it) |
| The distance at 90 m | **GREAT** (the high-frequency energy goes *down*: 7.376 → 7.001) |
| The meadow | **GREAT** (the plains preset is byte-identical) |
| The cost | **GREAT** (see below) |
| The matrix's warmth | NOT YET (cosmetic; it reads as stony ground) |
| The stones' shape (domes, not angular blocks) | NOT YET (cosmetic; the shape the distance field gives for free) |
| The rock's own material | NOT YET (**not this change's business** — the massif from 700 m is still a flat blue-grey wash) |

**The change as a whole: GREAT.** The scree is a scatter of stones with turf between them, the
boundary between the grass and the rock is no longer a line, the distance is calm, the meadow
is untouched, and the cost is nil.

### Measurements

- **The scree is discrete.** The coverage along an 8 m cut crosses the half-way mark at least
  **six** times (the CPU mirror's test), where a band's weight crosses it once or twice. Over
  half the cut is below half covered: the turf shows between the stones.
- **The sizes vary.** The stones' areas span more than 4:1 between the tenth and the ninetieth
  percentile, and their offsets inside their cells are spread (the mean near the middle, the
  spread above 0.2) — a scatter, not a grid.
- **The band is at the rock's foot.** Zero on the meadow and zero on the cliff, peaking at 26°,
  and at least six degrees wide at half its strength (a narrow band traced the hillside's
  break as a contour line).
- **The meadow is untouched.** The `plains` preset at 6 m is **byte-identical** — the mean
  170.725 and the standard deviation 26.575 before and after. The `hills` preset differs by
  0.3 DN, which is the scree on their slopes.
- **The distance is calm.** The frame's high-frequency energy (the laplacian's rms) at 25 m
  rises from **6.415 to 8.192** — the stones add real detail where they are visible — and at
  **90 m it falls, 7.376 → 7.001**: the scatter is band-limited, so at a distance the scree is
  a tone and not a corduroy. The mean barely moves (141.301 → 142.199 at 25 m), so the scree
  is a change of *texture* and not a brightening.
- **Cost.** `bench_elements`, High, 1920 × 1080, `mountains`, the minimum over the diagnostic
  history, the two builds run **back to back in one session** so the card's load is the same
  for both, three rounds each. The card's own state swings the *total* by 73 % between runs
  (1.87–3.24 ms for the *same* build), so the totals are not comparable and the per-pass
  minima are. The mountains' opaque pass, which is the pass the scree is in:

  | round | the baseline | this one |
  |---|---|---|
  | 1 | 0.147 ms | 0.167 ms |
  | 2 | **0.131 ms** | 0.164 ms |
  | 3 | 0.140 ms | **0.140 ms** |

  The minimum over the rounds is 0.131 ms against 0.140 ms: **+7 %**, within the note's 10 %,
  and the third round identical. The scree costs two cell-network lookups and one noise
  evaluation per pixel *in the band only* (`scree > 0.01`), and the meadow and the cliff pay
  nothing — the `plains` preset's capture is byte-identical.
- **The full gate.** `cargo test` 296 plus the 3 new scree tests, clippy, doc, wasm, the GPU
  integration suite (22 files), the lib GPU modules clouds (10), terrain (8), cover (8) and
  dust (6), and `the_horizon_sky` alone: all pass.

### The next five items, ranked

1. **The rock's own material is a texture where the world has objects.** The massif from 700 m
   is a flat blue-grey wash, and the face at 3 m is a uniform sandy grey with a fine even
   noise: the beds, the fractures and the grain are all colour modulations of *one continuous
   surface*, and there is no mechanism in the crate for a discrete object on a cliff. The
   scree found the mechanism — a cell network used the other way round — and the cliff's
   *blocks* are the same idea one scale up. This is the same root as the scree was, and the
   scree's fix says where to look.
2. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too — the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
3. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain` leak
   at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.
4. **A 25 mm/h storm hides the whole town under an orthographic camera**, carried. The rain's
   *readable* look is also less legible than the physical one (measured in iteration 28: the
   readable streaks carry 32 DN of contrast against the grass where the physical ones carry
   44), which is the defect the town capture keeps showing.
5. **A game's own light has no shadow of its own to fall on the crate's weather.** A lamp lights
   the town, but the crate's rain and snow are lit by the crate's lights alone, so a lamp in a
   downpour lights the *ground* and not the *rain*. A hook for a game's own lights in the
   precipitation and cloud passes would close it; the ownership map keeps the geometry in
   `bevy_flora`, but the lighting hook is this crate's.

## Iteration 35 — 2026-10-21 — the cliff is made of blocks: a rock face that breaks along its joints

**Commits:** `64f00cf` (the research note `docs/research/rock-blocks.md`), `863baad` (the
implementation with its tests), `3ed4d1d` (the tabular blocks and the joint from the nearest
point), `0e58b68` (the WESL's bound, the joint and the squash), `b119555` (the cells in order
of increasing distance, and the WESL note), `ebefdaa` (the mirror's ring order and the 3D
frame's threshold), the showcase image, this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The rock's own material is a texture where the world has objects** — the massif from 700 m is a flat blue-grey wash and the face at 3 m is a uniform sandy grey | 2 | **this iteration** |
| 2 | The dry patch's own texture (a tint on green blades) | 2 | carried |
| 3 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |
| 4 | A 25 mm/h storm hides the whole town under an orthographic camera | 2 | carried |
| 5 | A game's own light does not fall on the crate's weather | 3 | carried from iteration 33 |

Rank 1 beat the carried items because it is the *most prominent material in the crate* with the
*least* structure, and the measurement was damning: the material's own high-pass at 3 m was
**1.72 DN** — a fifth of the face's 8.34 DN of variation — and at 200 m the massif was **one
colour** (a max−min of 1.00 DN over a 300 × 200 patch, where the crate's own `rock-material.md`
asks for 9). The cause was structural, not a matter of tuning: every feature — the beds, the
fractures, the grain — was a *multiplier on the albedo of one continuous surface*, and there was
no mechanism anywhere in the crate for a discrete object. The overcast dome is class 1 and so
ranked higher by the class order, but its blocker is diagnosed and its fix is a *sky* change.

### What changed

- **The blocks are the cells of a 3D cellular network**, in the rock's own frame. A 2D network
  in the ground plane would stretch on a slope and weave, exactly as the crate's turf does.
- **The network's coordinates are squashed along the bedding plane's normal**, so the cells
  stretch into slabs: a rock breaks along *joint sets*, families of parallel planes, so its
  pieces are aligned rather than a random heap.
- **A block is *shaded*, not outlined**: its own tone (the cell's random factor), its own
  **facet** — a rock block's face is flat, and the *step* is at the joint — and a **joint** that
  is the cell's own *boundary*.
- **The coordinates are warped** before the lookup, so the blocks are irregular fragments and
  not a mosaic, and the joint's **width** is the cell's own draw, which is what sets the
  block's **size**.
- **The beds carry a value contrast**: the second tone was (1.06, 0.93, 0.76), whose luminance
  is 0.95, so the beds moved the luminance by 1.9 % and the massif was one colour.
- `cell3` in `noise.wesl` and its CPU mirror in `gpu/noise_mirror.rs`.

### Sources

- **Verdian, J. P., Sklar, L. S., Riebe, C. S., Moore, J. R. (2021)**, *Sediment size on talus
  slopes correlates with fracture spacing on bedrock cliffs*, Earth Surface Dynamics 9:1073,
  DOI `10.5194/esurf-9-1073-2021` (open access; the PDF was read in full, 914 lines). Used:
  "an obvious characteristic of columnar jointed rock is that it is **discontinuous in geometry
  while continuous in mechanics**" — a rock mass is a set of blocks held in one piece, and the
  discontinuity in geometry *is* the blocks; and the measurement, the finest talus clasts "as
  small as 2 mm, and median b-axis diameters ranging from 10 to 575 mm", with the median moving
  by more than 10-fold between sites. **Not indexed** (the ONNX out-of-memory error in
  `corpus.md`).
- **La Pointe, P. R., Hudson, J. A. (1985)**, *Characterization and interpretation of rock mass
  joint patterns*, SPE Formation Evaluation 1(1), DOI `10.1130/SPE199-P1` (the record was read;
  the PDF was not obtained). Named as the standard treatment of *joint sets* — the families of
  parallel planes along which a rock mass breaks, and the reason a cliff's blocks are tabular
  and aligned.
- **Dorsey, J. O'B., Edelman, A. S., Jensen, H. W., Legakis, J., Pedersen, H. K. (1999)**,
  *Modeling and rendering of weathered stone*, ACM SIGGRAPH 1999, DOI `10.1145/311535.311560`
  (the record was read; **the PDF was not obtained**).
- **Akenine-Möller, T., Haines, M., Hoffman, N., Shirley, P. (2019)**, *Real-Time Rendering,
  4th ed.* (corpus `bo/book_real_time_rendering_4th_akenine.md`). Used: §6.3 on the cellular
  texture — "a cellular texture is formed by measuring distances from each location to a set of
  'feature points' scattered through space. Mapping the resulting closest distances in various
  ways… creates patterns that look like cells, flagstones, lizard skin, and other natural
  textures" — and on Andersson's Frostbite terrain, where "geometric models, such as bushes and
  **rocks**, are placed within the scene".
- **Hill, S., Baker, D. (2012)**, *Rock-Solid Shading: Image Stability Without Sacrificing
  Detail*, SIGGRAPH *Advances* in Real-Time Rendering in Games course (corpus
  `s2/s2012-advances-pdf-slides-3.md`), on LEAN mapping: a detail normal's specular filtered by
  the *variance* of the normal, so a detail map does not alias at distance. **In the corpus**,
  read, **not indexed**.
- **The crate's own `rock-material.md`**: the beds, the joints and the grain, and the sentence
  that is the key to this change — "A Worley cell network cannot make them — it bounds *cells*,
  so a face reads as a giraffe's hide". The cells cannot make the *joints as drawn lines*, but
  the cells **are** the blocks, and a block is shaded rather than outlined.

### The critique and the verdicts (`/tmp/i35/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The blocks appeared, but as **dark holes in the grass**: the stone's colour multiplied the ground's albedo instead of replacing it, and the band sat on the soil's band rather than at the rock's foot | NOT YET |
| 2 | The stone's colour replaced the ground's, and the band moved to the rock's foot — but the relief scaled with the cell, so the smallest stones' rims tilted by 85° and rendered as black crescents | NOT YET |
| 3 | The relief is a dimensionless slope and the network is nested, and the matrix is the scree's own: the stones are countable at 3 m, the apron is wide, and the distance is calm | NOT YET |
| 4 | The matrix was still warm and the stones cast no shadow: the matrix is greyed toward the rock and the stones gained a contact shadow | **GREAT** |
| 5 | The blocks were a **mosaic** — every block the same size, the same shape, in a regular overlapping pattern that read as *shingles*. The warp breaks their shape, the joint's width sets their size, and the squash makes them tabular | **GREAT** |

| Feature | Verdict |
|---|---|
| The blocks, at 3 m | **GREAT** (irregular angular fragments, many sizes, tabular, each a flat facet) |
| The joints | **GREAT** (sharp, dark, continuous, and softened over the footprint) |
| The face at 25 m | **GREAT** (a cliff of slabs with the scree at its foot) |
| The distance at 90 m | **GREAT** (byte-identical to the baseline's: the blocks are band-limited away) |
| The meadow, the hills and the dunes | **GREAT** (byte-identical) |
| The material's visibility | **GREAT** (the high-pass 1.72 → 5.74 DN, the note's target is 3) |
| The blocks' size variation | NOT YET (a 3:1 range; a real cliff has more) |
| The massif's colour at 700 m | NOT YET (7.65 DN against the note's 9) |
| The cost | NOT YET (**+28 %** on the rock's pass, over the note's 10 %) |

**The change as a whole: GREAT.** The rock's material is no longer a texture where the world has
objects: a cliff face is a wall of blocks, the distance is calm, the meadow is untouched, and
the cost is a logged trade.

### Measurements

- **The material is visible.** The face at 3 m: the material's own high-pass (a 3 px blur
  subtracted) **1.72 → 5.74 DN**, the patch's standard deviation 8.34 → 15.09 DN and its
  max−min 43.35 → 89.47 DN. The note's first validation target (at least 3 DN) is met.
- **The distance is calm.** At 90 m the material's high-pass is **0.79 DN before and after** —
  the frame is *byte-identical*. The blocks are band-limited by the joint's own width, and the
  massifs carry the beds' broad tonal banding instead.
- **The meadow is untouched.** The `plains` preset at 6 m: the mean 170.725 and the standard
  deviation 26.575 **before and after** — byte-identical. The `hills` preset likewise.
- **The blocks are discrete, tabular and in the rock's own frame** — the three new tests:
  the joint's weight crosses the half-way mark many times along a cut (a wash crosses it
  once); the cells are *bigger* along the bedding than across it (17 against 18 the other way
  round before the squash was fixed); and the two heights 4 m apart disagree on a good fraction
  of the samples, which a 2D network in the ground plane would not.
- **Cost.** `bench_elements`, High, 1920 × 1080, `mountains`, the minimum over the diagnostic
  history, the two builds run **back to back in one session**, three rounds each. The card's
  own state swings the total by 73 % between runs (1.87–3.24 ms for the *same* build), so the
  totals are not comparable and the per-pass minima are. The mountains' opaque pass, which is
  the pass the blocks are in, over two independent paired sessions:

  | session | the baseline | this one | |
  |---|---|---|---|
  | 1 | **0.141 ms** | 0.181 ms | +28 % |
  | 2 | **0.153 ms** | 0.197 ms | +29 % |

  Over the note's 10 %, and the same figure twice.

  **The trade, logged.** The rock's own material goes from an sd of 1.72 DN to 5.74 DN for
  0.04 ms, and the cost is only paid where the rock shows: the meadow and the cliff's
  silhouette are byte-identical and at 90 m the whole frame is. Three measures brought it
  down from +44 %: the band-limit is known *before* the network is looked up, so the far terrain
  pays nothing; the joint is the *nearest* point's own cell's wall, so the lookup computes one
  distance and not two; and the cells are visited in order of increasing distance, so the
  prune's bound tightens and about seven of the twenty-seven cells are hashed instead of
  fourteen. The next measure is a smaller *block's* cell and a cheaper warp.
- **The full gate.** `cargo test` 265 plus the 3 new block tests, clippy, doc, wasm, the GPU
  integration suite (22 files), the lib GPU modules clouds (10), terrain (8), cover (8) and
  dust (6), and `the_horizon_sky` alone: all pass.

### A note on the WESL, for whoever touches this next

The crate's shaders are **WESL**, and two of its rules bit during this iteration:

- **The WESL parser rejects `abs()` on a compound expression.** The failure is at *runtime*,
  in the GPU driver, and it is **silent**: the terrain's material fell back to a default, the
  whole face rendered as a flat grey, and the capture's high-pass collapsed from 5.83 DN to
  0.31. The error is in the log ("failed to create shader module: … parsing error") and it
  is easy to miss among the capture's other output.
- **`pass` is a WGSL reserved word**, like `active` and `target` and `patch`.

The fix for both is a **validator**: the `wesl` crate (the one Bevy uses) compiles a module to
WGSL, and `naga` parses it, in a second and without a GPU. `/tmp/wesltest` is that validator;
it found the first of the two in the time it takes to run one capture.

### The next five items, ranked

1. **The massif's colour at a distance.** The beds now carry a value contrast, but the note's
   target is a 9 DN spread over the massif from 700 m and the measurement is 7.65. The beds
   are at a 4.5 m spacing on a tilted plane, so their banding is a broad wash; the fix is
   beds that *stack* — a bed's own thickness and the next one's value — so a distant massif
   shows layers and not a gradient.
2. **The blocks' size variation.** A 3:1 range is not enough; a real cliff's blocks range from
   a metre to a hand's width, and the cell's *boundary* is the only place a second size can
   come from. The scree's nested networks are the wrong instrument here — the blocks are
   *pieces of one rock*, and a cell's boundary is where a piece would split.
3. **The cost of the blocks**, +28 % on the rock's pass. A smaller block's cell and a cheaper
   warp are the two levers left.
4. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too — the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
5. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain` leak
   at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.

## Iteration 36 — 2026-10-21 — beds that stack: a distant cliff is a succession, not a stripe pattern

**Commits:** `3e7334c` (the research note `docs/research/bedding.md`), `46c0b62` (the note's
Deviations), `7370775` (the implementation with its tests), `b34465d` (the README, the changelog
and the showcase image), this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The massif's colour at a distance** — the beds are smeared by their own warp and every bed is identical, so the massif averages to a flat blue-grey haze | 2 | **this iteration** |
| 2 | The blocks' size variation (a 3:1 range against a real cliff's metre-to-hand's-width) | 2 | carried |
| 3 | The cost of the blocks, +28 % on the rock's pass | 5 | carried |
| 4 | The dry patch's own texture | 2 | carried |
| 5 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |

Rank 1 beat the carried items because the massif is what a game shows when it shows *any*
landscape, and it read as haze: over a rock-only 200 × 200 patch from 120 m the spread was a
max−min of **1.00 DN** — one colour — where the crate's own `rock-material.md` asks for 9. The
green turf and the ochre soil read clearly at the same distance, so it was not the resolution
that was failing; the rock had no structure to resolve. Candidate 3 is a real cost, but it is a
*tuning* problem with a known shape (a smaller cell, a cheaper warp) and no visible symptom, and
a performance number without a visible defect is worth less than a visible haze.

### What changed

- **The beds are a warped lattice**: the boundary between the beds whose index is `i` and
  `i + 1` falls at `i + warp(i)`, and a bed's *thickness* is the gap between two boundaries.
- **A per-bed tone**, drawn from the bed's index with the crate's own integer hash, because *a
  bed is a colour*: a sandstone, a shale, a limestone.
- **The bed's edge is band-limited away with the distance and the bed's tone is not**, which is
  the whole trick: at a distance the cliff shows its succession and not its stripes.
- The warp's amplitude drops from 0.78 of a bed to a fifth, so the beds undulate and stay bands.

### Sources

- **Talling, P. J. (2001)**, *On the frequency distribution of turbidite thickness*, Journal of
  Sedimentary Research 71(3), DOI `10.1046/j.1365-3091.2001.00423.x` (the record and its
  abstract were read; **the PDF was not obtained**). Used: "the nature of this thickness
  distribution is currently uncertain, with log-normal or negative-exponential frequency
  distributions and power-law cumulative frequency distributions having been proposed", and the
  result — "turbidite bed thickness and sand-interval thickness within each bed have a
  frequency distribution comprising **the sum of a series of log-normal frequency
  distributions**". **Not indexed.**
- **Longhitano, S. G., Nemec, W. (2005)**, *Statistical analysis of bed-thickness variation in a
  Tortonian succession*, Sedimentary Geology 178, DOI `10.1016/j.sedgeo.2005.05.006`; **Bádenas,
  B. et al. (2003)**, *Sequence stratigraphy and bedding rhythms…*, Palaeogeography 198, DOI
  `10.1016/s0037-0738(03)00099-x`; and **Lowey, G. W. (1992)**, *Variation in bed thickness in a
  turbidite succession…*, Sedimentary Geology 150, DOI `10.1016/s0037-0738(92)90021-i` — the
  records were read and **the PDFs were not obtained**. Named as the treatments of bed-thickness
  variation, of bedding *rhythms* and of the *cycles* in a succession's thicknesses.
- **Hill, S., Baker, D. (2012)**, *Rock-Solid Shading: Image Stability Without Sacrificing
  Detail*, SIGGRAPH *Advances* in Real-Time Rendering in Games course (corpus
  `s2/s2012-advances-pdf-slides-3.md`), on the anti-aliasing principle — **estimate the
  variance and widen the filter** rather than let a sub-pixel feature alias (Toksvig's
  σ² = (1 − |Nₐ|)/|Nₐ|, and LEAN mapping's direct variance estimation). For the *albedo* the
  crate's own `detail_keep` is the instrument: a band whose thickness is below the pixel's
  footprint must be **cancelled**, not averaged. **In the corpus**, read, **not indexed**.
- **The crate's own `rock-material.md`**: the beds, and the two sentences that frame the change
  — "The bed's coordinate is a *tilted* height, so the beds cut across the terrain at an
  angle, and the warp tilts them further" (the *warp* is right; its amplitude is not), and
  "a bed is a *band* of rock with an edge, not a gradient" (the edge is right; the band is
  not).

### The critique and the verdicts (`/tmp/i36/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The beds' tone was gated by the edge's band-limit, so at 120 m — the distance the tone is *for* — the tone was cancelled and the massif was back to being a haze | NOT YET |
| 2 | The tone is a low-frequency feature and the edge is not: the tone must not be gated by the edge's band-limit. The material's own contribution at 120 m is a standard deviation of 3.41 DN and a max−min of 21.2 DN, and the face at 3 m carries bands of ±7 DN | **GREAT** |

| Feature | Verdict |
|---|---|
| The massif's colour at 120 m | **GREAT** (the material's own sd 3.41 DN, the max−min 21.2 DN) |
| The face at 3 m | **GREAT** (bands of ±7 DN alternating) |
| The dusk and the dawn | **GREAT** (the beds read as layers in the raking light) |
| The distance at 300 m | **GREAT** (no corduroy; the bands fade into the haze) |
| The meadow | **GREAT** (the plains and hills are byte-identical) |
| The cost | **GREAT** (+3.5 % on the mountains' opaque pass) |
| The bands' width regularity | NOT YET (a smooth warp's derivative is bounded, so the widths are nearly uniform) |
| The seams' aliasing at 3 m | NOT YET (cosmetic; the edge is softened over the footprint, but a line a fraction of a bed wide still steps) |

**The change as a whole: GREAT.** The massif is no longer one colour, the beds follow the
bedding plane, the meadow is untouched, and the cost is +3.5 %.

### Measurements

- **The massif is not one colour.** Over a rock-only 200 × 200 patch of the massif from 120 m,
  the material's own contribution (the before/after difference) has a standard deviation of
  **3.41 DN** and a max−min of **21.2 DN**, against the baseline's max−min of **1.00 DN**.
- **The face at 3 m** carries bands of ±7 DN: the vertical profile of the material's change is
  −3.7, −2.0, −0.9, +0.7, +1.0, +0.4, then **+5.5, +6.6, +7.3, +6.0, +4.1**, then −5.4, −6.3,
  −7.6 — the succession reading as bands and not as a wash.
- **The meadow is untouched.** The `plains` preset at 6 m: the mean 170.725 and the standard
  deviation 26.575 **before and after** — byte-identical. The `hills` preset likewise.
- **The distance is calm.** At 300 m the beds are visible on the near slopes and there is no
  corduroy: the edge is cancelled with the distance and the tone survives.
- **Cost.** `bench_elements`, High, 1920 × 1080, `mountains`, the minimum over the diagnostic
  history, the two builds run **back to back in one session**, three rounds each. The
  mountains' opaque pass: the minimum over the rounds is **0.197 ms on the baseline and
  0.204 ms on this one, +3.5 %** — well inside the note's 10 %. The bed's index is a
  four-step fixed point, and it is cheap next to iteration 35's cell lookup.
- **The full gate.** `cargo test` 301 plus the 2 new bed tests, clippy, doc, wasm, the GPU
  integration suite (22 files), the lib GPU modules clouds (10), terrain (8), cover (8) and dust
  (6), and `the_horizon_sky` alone: all pass.

### The next five items, ranked

1. **The blocks' size variation.** A 3:1 range against a real cliff's metre-to-hand's-width. The
   cell's *boundary* is the only place a second size can come from, and the scree's nested
   networks are the wrong instrument here — the blocks are *pieces of one rock*.
2. **The cost of the blocks**, +28 % on the rock's pass. A smaller block's cell and a cheaper
   warp are the two levers left.
3. **The beds' width regularity.** The thicknesses' spread is 1.4:1, where a log-normal's would
   be 3:1; a cumulative sum would give the heavier tail, and it is the one thing the shader
   cannot afford.
4. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too — the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
5. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain` leak
   at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.

## Iteration 37 - 2026-10-21 - the cliff is not a quilt: a rock face breaks into irregular fragments

**Commits:** `e3d6070` (the research note `docs/research/rock-fragments.md`), `33a19fa` (the
note's Deviations), `037d6b1` (the implementation with its tests), `592edb8` (the README, the
changelog and the showcase image), `832859b` (the crate's own WESL validator), this log.
**Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The cliff is a quilt** - the blocks are a regular lattice of identical lozenges, every block the same size and shape | 2 | **this iteration** |
| 2 | The blocks' *tones* are narrow: the fragments are all the same grey | 2 | carried (the next item) |
| 3 | The cost of the blocks, +28 % on the rock's pass | 5 | carried |
| 4 | The dry patch's own texture | 2 | carried |
| 5 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |

Rank 1 beat the carried items because the *cliff* is the crate's most prominent material and
it read as **wallpaper**: the dusk capture showed a diagonal quilt of same-size diamonds, every
block with the same dark notch on the same side, in rows. A player reads that as a texture, and
the whole point of iteration 35 was that a cliff is not a texture. Rank 2 is the same material
and would have been the natural next step, but it is a *tuning* change on a structure that was
still a lattice, so it would have polished a quilt.

### What changed

- **The warp is back**, and it is the noise's own *gradient* - one evaluation for a whole 3D
  vector field, where three separate noise *values* would be three lattice walks.
- **The warp's amplitude** is calibrated for the gradient (whose mean magnitude is 0.94) and
  not for the value: 0.30 m, a mean displacement of 0.20 of a cell.
- **The warp's scale** is nine cells, so it *bends* the lattice coherently instead of
  scrambling it.
- **The cells are unequal**: the network's feature points are drawn *towards* their cell's
  centre, so a point near its cell's corner leaves a big cell behind it and a point at the
  centre a small one. That is the only place a cell network's cells can differ in size.
- **The joint's width is a Weibull** draw, the shape the fracture spacing's distribution
  follows.
- **The block's cell is 0.95 m** and not 1.5 m, and the joint's darkening is a *shadow* (0.76)
  with the relief at 0.95, so the sun rakes each fragment's edge.
- `CELL_INSET` in `noise.wesl` and its CPU mirror.

### Sources

- **Verdian, J. P., Sklar, L. S., Riebe, C. S., Moore, J. R. (2021)**, *Sediment size on talus
  slopes correlates with fracture spacing on bedrock cliffs*, Earth Surface Dynamics 9:1073-1090,
  DOI `10.5194/esurf-9-1073-2021` (open access; the PDF was downloaded to
  `/tmp/research/verdian2021.pdf` and read in full, 914 lines of `pdftotext -layout`). Used:
  "fracture spacing distributions in rock commonly have shapes that follow one of these
  distributions (Gillespie et al., 1993)" and the result, that "the **Weibull** form of the
  exponential distribution (Weibull, 1951) yielded the best fit to the data in most cases";
  the measurement, the spacings running "from 2 to 5000 mm" with the site medians "from 10 to
  390 mm" and the **geometric standard deviation** 0.214 to 0.551 across the fifteen sites; and
  the *coupling*, "the close site-to-site correspondence between the spread in the b-axis
  diameter distribution and the spread in the fracture spacing distribution". **Not indexed.**
- **Yao, W., Sharifzadeh, M., Yang, Z. (2020)**, *Assessment of block size distribution in
  fractured rock mass...*, DOI `10.1063/1.5143906`; **Azizi, A., Moomivand, H. (2021)**, *A New
  Approach to Represent Impact of Discontinuity Spacing and Rock Mass Description on the
  Median Fragment Size of Blasted Rocks...*, DOI `10.1007/s00603-020-02360-4`; and **Park, L.
  et al. (1993)**, *In-situ block size assessment from discontinuity spacing data*,
  International Journal of Rock Mechanics & Mining Sciences, DOI `10.1016/0148-9062(93)90982-j`
  - found by `paper_search` and their records read, but **no PDF was obtained** (the publishers
  returned a landing page or a paywall). Named as the treatments of the block size
  distribution from the discontinuity spacing, which is the model the note adopts.
- **The crate's own `rock-blocks.md`** (iteration 35): the blocks, the joint and the warp, and
  the three sentences that frame the change - "The cells of a network are all the same size, so
  without this every block would be the same and the face would read as a cobblestone" (the
  *joint's width* was the answer, and it did not work: the **cells** are still the same size),
  "a rock breaks into irregular fragments, and a warp is what makes the fragments irregular"
  (the warp was the answer, and it was never implemented), and "a cell whose joint is wide is a
  *small* piece of rock" (the *size* was tied to the joint's width, so the block's size varied
  while its *cell* did not).
- **Akenine-Moller, T., Haines, M., Hoffman, N., Shirley, P. (2019)**, *Real-Time Rendering,
  4th ed.* (corpus `bo/book_real_time_rendering_4th_akenine.md`): "a cellular texture is formed
  by measuring distances from each location to a set of 'feature points' scattered through
  space... creates patterns that look like cells, flagstones, lizard skin, and other natural
  textures" - and the note that a cell network's cells are *the same size*, the property the
  crate's own rock note had already called out.

### The critique and the verdicts (`/tmp/i37/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The warp was **off** - the commit that asked for the noise's gradient left `vec3<f32>(0.0)` - and the cells were a perfect lattice | NOT YET |
| 2 | The warp's amplitude was calibrated for the noise's *value* and not for its *gradient*, whose mean magnitude is 0.94: it displaced a boundary by 0.06 of a cell | NOT YET |
| 3 | The warp's *scale* was the cell's own, so it **scrambled** the lattice instead of bending it: the cell's index stopped being monotonic along a cut and the joint was crossed zero times in 30 m | NOT YET |
| 4 | The cells are still all the same size, and the CPU mirror's `fract` was Bevy's, which is negative for a negative coordinate: the joint's weight was zero on half the world | NOT YET |
| 5 | The face was a **giraffe's hide** - a network of thin dark lines with flat blocks between them. A joint is a *shadow*, and a shadow is wider than the crack | NOT YET |
| 6 | The cell at 0.95 m and not 1.5 m: the face reads as a fracture network, and the fragments are countable | **GREAT** |

| Feature | Verdict |
|---|---|
| The face at 3 m | **GREAT** (irregular fragments of many sizes, each with its own lit facet) |
| The dusk raking light | **GREAT** (the seams are shadowed grooves and the sun finds each fragment's edge) |
| The face at 12 m and at 30 m | **GREAT** |
| The distance at 120 m | **GREAT** (byte-identical: the blocks are band-limited away) |
| The meadow | **GREAT** (the plains preset is byte-identical) |
| The cost | see the measurements |
| The fragments' *tones* | NOT YET (all the same grey; a real cliff has a sandstone beside a basalt) |
| The blocks' size spread | NOT YET (real and visible, but one family of size) |
| The joints' softness | NOT YET (cosmetic; a few of the wider seams read as smudges) |

**The change as a whole: GREAT.** The quilt is gone, the fragments are irregular and many-sized,
the distance is calm, the meadow is untouched.

### Measurements

- **The quilt is gone.** The blocks' **edge-orientation resultant** - the circular resultant of
  the edge angles, which is how strongly the blocks' edges agree on a direction, and so the
  quilt's signature - falls from **0.431 to 0.277** on a face at 3 m. The measure is the one
  the note did not have and the defect needed: a lattice of equal cells gives every block's
  edge the same direction, and rock's edges run every which way.
- **The material is *more* visible.** The material's own high-pass over a 500 x 340 patch of
  the face at 3 m: **6.67 DN before, 8.02 DN after**. The dark fraction (the fragments'
  shadows) rises from 16.8 % to 22.7 %, and the seam spacings measured from 1497 to 2750.
- **The distance is calm.** The view from 120 m: the blocks are band-limited away and the
  massif carries the beds' banding. Over a 500 x 300 patch of the far massif the material's
  own high-pass is **4.3440 DN before and 4.3440 DN after** - the same to four decimals, so
  the blocks contribute nothing measurable at that distance. (The two files are not
  byte-identical: 12 225 of 921 600 pixels differ, by up to 16 DN, which is the GPU's own
  non-determinism in the sky's gradient and not the rock's material.)
- **The meadow is untouched.** The `plains` preset at 6 m is **byte-identical**.
- **Cost.** `bench_elements`, High, 1920 x 1080, `mountains`, the **minimum over the
  diagnostic history**, the two builds run **back to back in one session**, three rounds each.
  The mountains' opaque pass, which is the pass the blocks are in:

  | round | the baseline | this one |
  |---|---|---|
  | 1 | 0.221 ms | 0.226 ms |
  | 2 | 0.219 ms | 0.579 ms (the card was busy) |
  | 3 | 0.219 ms | 0.228 ms |

  The **minimum over the rounds** is **0.219 ms on the baseline and 0.226 ms on this one, a
  3.2 % regression** — inside the note's 10 %. The baseline's three readings agree to within
  0.002 ms, so the card was quiet; the 0.579 is contention and the two clean rounds read
  0.226 and 0.228.

  The cost is the **warp**: one `perlin3_d` per rock pixel, and the cell is smaller (0.95 m and
  not 1.5 m), so the network's cells are 2.4x denser and the prune visits more of them. The
  warp is *inside* the band-limit, so the far terrain pays nothing for it, and the view from
  120 m is byte-identical.
- **The full gate.** `cargo test` 269 plus the 3 new block tests, clippy, doc, wasm, the GPU
  integration suite (22 files), the lib GPU modules clouds (10), terrain (8), cover (8) and dust
  (6), and `the_horizon_sky` alone: all pass.

### The crate's own WESL validator (`tools/wesl-check`)

This iteration lost a full capture cycle to a **silent** shader error: the terrain came out
black because a struct gained a field and its construction did not, and the only clue was a
`failed to process shader` line in the run's log, which a capture script throws away. A WESL
error is silent at build time, because the shaders are embedded with `embedded_asset!`.

So the crate now has its own validator: it compiles **every** one of the crate's WESL modules
to WGSL and parses the result with naga - the same compiler and the same parser Bevy uses -
in about twenty seconds and without a GPU, and it names the line. Bevy's own modules are
stubbed, because they are not on disk; a stub that is missing is reported as "not checked"
and is **not** counted as a failure, so a missing stub cannot hide a real error.

```sh
cargo run --release --manifest-path tools/wesl-check/Cargo.toml -- src
```

### The next five items, ranked

1. **The fragments' tones.** The block's tone is the cell's factor at +/-0.22, so the
   fragments are all the same grey; a real cliff has a sandstone beside a basalt. The geology
   is clear about this - a bed *is* a colour - and the crate's beds already carry the large
   scale, but the *fragments* do not differ in colour. The cell's factor is already drawn and
   already free.
2. **The blocks' size spread.** Real and visible, but one family of size: a cliff also carries
   the small chips and the big slabs in the same wall. The second generation of blocks was
   tried and rejected (a speckle), so the spread comes from the cells' unequal sizes alone.
3. **The cost of the blocks**, +28 % on the rock's pass in iteration 35, and this iteration's
   warp on top of it. A smaller cell is now in, so the *number* of cells went up; the prune's
   bound and the band-limit are the two levers left.
4. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too - the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
5. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain`
   leak at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.

## Iteration 38 - 2026-10-21 - the cliff is not one grey: a rock face carries a palette

**Commits:** `37f763c` (the research note `docs/research/rock-palette.md`), `c436024` (the
implementation with its tests), `6927c20` (the note's Deviations), `647d3b6` (the README, the
changelog and the showcase image), this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The cliff is one grey** - the whole face is a single hue, the resultant 0.998 at dusk, and the rock-only patch at 3 m has a chroma's median of 0.0157 | 2 | **this iteration** |
| 2 | The palette does not reach the distance: the blocks are band-limited away at 120 m, so a distant massif is still one cool grey | 2 | carried (the next item) |
| 3 | The blocks' size spread: real and visible, but one family of size | 2 | carried |
| 4 | The cost of the blocks, +28 % on the rock's pass in iteration 35 and this iteration's warp on top of it | 5 | carried |
| 5 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |

Rank 1 beat the carried items because it is in **every frame that shows a cliff**, and it is
the last thing the rock's own material was missing: iteration 35 gave the cliff its *pieces*,
iteration 36 gave the massif its *succession*, and iteration 37 gave the fragments their
*irregularity* — and all three left the rock **one colour**. The measurement is damning and
simple: at dusk the face is a single hue with a spread of 2.1°, and the cause is structural
(the whole rock system is a set of *scalars*), so no amount of tuning would have fixed it.

### What changed

- **The rock's tone is a `vec3`, not a scalar.** A scalar can only make a rock lighter or
  darker; a colour can make it another *rock*.
- **A palette of four rocks** — a cool basalt, an iron-stained sandstone, a pale limestone, a
  dark ferromanganese varnish — mixed as **convex shares** by a score, exactly as the ground's
  palette is (iteration 30).
- **The score is two narrow splits**: the cell's own factor (the *selection*) splits the
  palette into its cool half and its warm half, and the cell's *second* draw (the
  *distribution*) splits each half again. So a fragment is a **piece of one rock** and the
  colour varies from fragment to fragment.
- **The palette's equal-weight mean is exactly (1, 1, 1)**, so the rock's mean colour and mean
  brightness cannot move.
- **A rind**: the film of iron and manganese on the surface, a separate layer with its own
  colour and a patchy coverage, band-limited away with the distance.
- `Cell3::factor2` in `noise.wesl` and its CPU mirror: a **second, independent** hash of the
  cell, because one hash gives one draw and the palette wants two.

### Sources

- **Tansem, K., Storemyr, P. (2020)**, *Red-coated rocks on the seashore: The esthetics and
  geology of prehistoric rock art in Alta, Arctic Norway*, Geomorphology 395, DOI
  `10.1002/gea.21832` (the record and the abstract were read; **the PDF was not obtained** —
  Wiley returns 403). Used, and it is the model this note adopts: "the pronounced **red**
  bedrock surfaces in the current seashore zone are composed of **inorganic iron films** related
  to a high content of magnetite in the native sandstone"; and the sentence that is the whole
  change: "it is highly likely that the rock art was originally carved on rocks with **red iron
  films, rocks that are now generally gray**" - *the same rock* wears a red film in one place
  and is grey in another, and the film's own colour is what the surface shows.
- **Krinsley, D., Pye, K., Boggs, S., Tovey, D. (1998)**, *Desert (rock) varnish*, in *Backscattered
  Scanning Electron Microscopy and Image Analysis of Sediments and Sedimentary Rocks*,
  Cambridge University Press, DOI `10.1017/cbo9780511628894.008` (the record and its
  introduction were read; **the PDF was not obtained**). Used: "Desert (rock) varnish is a thin
  (**< 100 to 500 um thick**) coating of **ferromanganese oxides, clay minerals, and trace
  elements**, which can form on sedimentary, igneous, or metamorphic rocks with stable surfaces";
  and that it is "a special kind of microscale sedimentary deposit" with "numerous ultrathin
  laminae that commonly grade into irregular pods of jumbled material" - so a varnish is
  **layered** and **patchy**, and it is a *coating on* the rock and not the rock.
- **Reneau, S. L. (1993)**, *Manganese Accumulation in Rock Varnish on a Desert Piedmont, Mojave
  Desert, California, and Application to Evaluating Varnish Development*, Quaternary Research
  40(1), DOI `10.1006/qres.1993.1084` (the record and the abstract were read; **the PDF was not
  obtained**). Used: "Rock varnish coatings tend to become **thicker, darker, and more
  continuous** over time, leading to the use of changes in overall varnish color and the
  percentage of clast surfaces covered by varnish as relative-age indicators" - so the varnish's
  *coverage* is a fraction of the surface, and its colour deepens with it. **Not indexed.**
- **Potter, R. M., Rossman, G. R. (1979)**, *The manganese- and iron-oxide mineralogy of desert
  varnish*, Geochimica et Cosmochimica Acta 43, DOI `10.1016/0009-2541(79)90085-8` (the record
  was read; the PDF was not obtained). Named as the mineralogy of the varnish.
- **Papavasiliou, N. (2015)**, *Real-Time Grass (and Other Procedural Objects) on Terrain*,
  JCGT 4(1):26-49 (`https://jcgt.org/published/0004/01/02/paper.pdf`). **This is the model for
  the mixing**, and it is the crate's *own* precedent: the ground's palette (iteration 30) is
  this model's implementation, and the rock's palette is the same construction on a different
  material. Used: the **reference colour** and the **score**, in HSV so that "the HSV color
  space is more suitable, both for the definition of the reference colors and for the
  calculation of color distances"; the **weight vector** `w`, which "allows us to ignore certain
  color components during the calculation of the distance ... where a reasonable choice is gray
  for which hue is meaningless", and the note that it "is also useful in many cases to discard
  the lightness component, as its variation over the terrain is often the result of shadowing
  effects, which are unrelated to terrain type"; the **exponent** `p`; and the **score-weighted
  palette blend** (eq. 17), `C = sum s_i T_i / sum s_i`. **Not indexed.**
- **Andersson, J. (2007)**, *Terrain rendering in Frostbite using procedural shader splatting*,
  SIGGRAPH 2007 *Advances* ch. 5 (corpus `10.1145_1281500.1281668`): the **Overlay** blend, "a =
  base < 0.5 ? 2*base*value : 1 - 2*(1-base)*(1-value)", which "doesn't affect areas where base
  mask is 0.0 or 1.0" - the instrument for a *modulation* that must not move the mean, and the
  reason the crate's palette is mixed as **convex shares** rather than as a modulation.
- **The crate's own `ground-colour.md`** (iteration 30): the palette, the score, the convex
  shares, and the three failures that fixed them - "by the unweighted mean of the tones the
  ground's mean brightness drifted 3.2 DN, by the mixture's own colour the hue was cancelled
  entirely, and by a per-channel mean the hue was cancelled again". The rock's palette is that
  note's construction, and it inherits its discipline.
- **The crate's own `rock-material.md`** (iteration 29): the rock's albedo `(0.072, 0.069,
  0.067)` - "a stone face under a blue sky is grey with the blue in it" - and the "second, darker
  rock colour" mixed in by the strata. The second tone is a *fixed* `vec3`, so the strata's
  colour is one colour and not a palette.

### The critique and the verdicts (`/tmp/i38/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The palette's tones were written as raw RGB triples and came out at saturations of **0.84 and 0.75** - a strong blue and a strong orange, which are not rock colours but *primaries*, and the cliff read as a rust-painted fence | NOT YET |
| 2 | The palette's equal-weight mean was 0.93 and the face's mean luminance fell **2.7 DN** - a game that changes colour changes its lighting | NOT YET |
| 3 | The distribution was a *field* at six cells, so the cliff read as **blotches** - coloured regions with every fragment in a region one rock | NOT YET |
| 4 | The palette is written in **hue and saturation** and normalised so its mean is exactly (1, 1, 1), and the distribution is the cell's *second* draw, so the colour varies from fragment to fragment | **GREAT** |

| Feature | Verdict |
|---|---|
| The face at 3 m | **GREAT** (the fragments carry their own colour, and it varies from fragment to fragment) |
| The dusk | **GREAT** (warm sandstone and pale limestone, cool grey in the crevices, dark varnish) |
| The dawn | **GREAT** (a cool blue shadowed face with warm ochre fragments) |
| The face at 12 m | **GREAT** |
| The distance at 120 m | **GREAT** (unchanged to four decimals; the blocks are band-limited away) |
| The meadow | **GREAT** (7 of 921 600 pixels differ, by 1 DN) |
| The cost | see the measurements |
| The palette at a *distance* | NOT YET (the blocks are band-limited away, so a distant massif is still one cool grey - the next item) |
| The varnish's red | NOT YET (cosmetic; the tone is a multiplier on a cool grey and the multiplication pushes it warm) |
| The rind's patches | NOT YET (cosmetic; the film is there but does not read as a *layer*) |

**The change as a whole: GREAT.** The cliff is a succession of different rock, the mean does
not move, the meadow is untouched, and the distance is calm.

### Measurements

- **The face is not one hue.** Over a rock-only 500 x 340 patch of the face at 3 m: the hue's
  **resultant falls from 0.800 to 0.656** and the hue's **spread rises from 27.6 to 39.7
  degrees**. Over the whole face: the resultant 0.853 → 0.719 and the spread 21.1° → 34.2°.
  The resultant is the measure the note did not have and the defect needed: a cliff of one rock
  has every fragment on the same hue, and the circular resultant of the hue angles is near 1.
- **The rock is a colour and not a grey.** The chroma's median over the rock-only patch:
  **0.0157 → 0.0275**; over the whole face 0.0275 → 0.0353.
- **The mean does not move.** The face's mean luminance at 3 m: **112.99 → 112.61 DN**, a
  0.38 DN drift, because the palette's equal-weight mean is exactly (1, 1, 1) and the mixture
  is a convex combination of the tones. The first version's mean was 0.93 and the face fell
  2.7 DN.
- **The distance is calm.** The view from 120 m: the material's own high-pass **4.3440 DN
  before and after**, the same to four decimals. The blocks are band-limited away and the
  massif carries the beds' banding.
- **The meadow is untouched.** The `plains` preset at 6 m: **7 of 921 600 pixels differ, by 1
  DN**, in a band across the middle of the frame - the GPU's own non-determinism and not the
  palette, which is applied to the rock and not to the turf.
- **Cost.** `bench_elements`, High, 1920 x 1080, `mountains`, the **minimum over the diagnostic
  history**, the two builds run **back to back in one session**, and the card's state logged
  throughout. The mountains' opaque pass, which is the pass the palette is in:

  | session | the baseline | this one |
  |---|---|---|
  | 1 | **0.224 ms** | **0.227 ms** |
  | 2 | 0.228 ms | 0.262 ms |
  | 3 | 0.224 ms | 0.227 ms |

  The **minimum over the rounds** is **0.224 ms on the baseline and 0.227 ms on this one, a
  1.3 % regression** — the palette is nearly free, which is what the construction says: the
  cell's factor and the cell's second factor are **already drawn** (the joint's Weibull uses
  the first), and the mixture is four multiplies, a divide and a `select`.

  **The card was contended for most of the session.** The two resident servers (scribe at
  2.6 GB and distill at 7.5 GB) share the RTX 3090, and a *third* process was intermittently
  using it: most readings were 0.56-0.66 ms for **both** builds, which is the card's state and
  not the shader's. The three rows above are the readings that were taken while the card was
  quiet, and the two builds' readings agree to within 0.004 ms in the quiet ones. The
  minimum over the whole session is the same for both: 0.224 and 0.227.
- **The full gate.** `cargo test` 271 plus the 2 new palette tests, clippy, doc, wasm, the GPU
  integration suite (22 files), the lib GPU modules clouds (10), terrain (8), cover (8) and dust
  (6), and `the_horizon_sky` alone: all pass.

### The next five items, ranked

1. **The palette does not reach the distance.** The blocks are band-limited away at 120 m and
   the massif carries the *beds'* banding and the beds' own tone, so a distant massif is still
   one cool grey - and a game that shows a mountain from a kilometre away sees no variety at
   all. The beds carry the distance by design (iteration 36), and their *tone* is a scalar,
   so the **beds** are where the palette has to go: a bed's own colour, drawn as a *hue* and
   not as a brightness. The same palette, at the bed's scale.
2. **The blocks' size spread.** Real and visible, but one family of size: a cliff also carries
   the small chips and the big slabs in the same wall. The second generation of blocks was
   tried and rejected (a speckle), so the spread comes from the cells' unequal sizes alone.
3. **The cost of the blocks**, +28 % on the rock's pass in iteration 35, and the two warps on
   top of it. A smaller cell is now in, so the *number* of cells went up; the prune's bound
   and the band-limit are the two levers left.
4. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too - the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
5. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain`
   leak at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.

## Iteration 39 - 2026-10-21 - a distant range is many rocks: the beds carry the colour to the horizon

**Commits:** `357e73c` (the research note `docs/research/bed-colour.md`), `9604cfc` (the
implementation), `66ab5e8` (the CPU mirror and the two tests), `fa7ca42` (the note's Deviations),
`329a7dc` (the README, the changelog and the showcase image), this log. **Sibling repos:**
none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The palette does not reach the distance** - a distant massif is still one cool grey, and a game that shows a range from a kilometre away sees no variety | 2 | **this iteration** |
| 2 | The blocks' size spread: real and visible, but one family of size | 2 | carried |
| 3 | The cost of the blocks, +28 % on the rock's pass in iteration 35 and the two warps on top of it | 5 | carried |
| 4 | The dry patch's own texture | 2 | carried |
| 5 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |

Rank 1 beat the carried items because it is the **last** thing the rock's own material was
missing, and iteration 38 made it plain: the palette reached the cliff's *fragments* and
stopped there, and the fragments are band-limited away at 120 m. So the crate could render a
cliff with four different rocks and a mountain range with **one**. A range of mountains is the
most common thing a game shows, and it was a single grey.

### What changed

- **The bed's tone is a `vec3`, not a scalar.** A scalar can only make a rock lighter or
  darker; a distant range is many *colours*.
- **The bed's colour is a draw on the bed's index**, from the fragments' palette, and the
  draw's *quantity* is the bed's **iron**: the Q7/4 ratio is the standard quantitative
  description of a sediment's colour, and goethite is yellow while hematite is red.
- **A region** - a slow field at the massif's scale (260 m) - biases the bed's selection, so a
  whole massif leans towards one rock and the massifs separate into warm and cool. A range of
  mountains is made of *formations*, and a formation is a massif.
- **The bed's colour is band-limited against the fragments**, with the fragments' own limit
  (the joint's width), so the two are band-limited against each other exactly once.

### Sources

- **Debret, M., Sebag, D., Desmet, M., Balsam, W., Copard, Y., Mourier, B., Susperriguui, F.,
  Arnaud, F., Bentaleb, I., Chapron, E., Lallier-Verges, E., Winiarski, T. (2011)**,
  *Spectrocolorimetric interpretation of sedimentary dynamics: The new "Q7/4 diagram"*,
  Earth-Science Reviews 106, DOI `10.1016/j.earscirev.2011.07.002` (open access; the PDF was
  downloaded to `/tmp/research/debret2011.pdf` and read in full, 2716 lines of
  `pdftotext -layout`). Used, and it is the model this note adopts:
  - The **Q7/4 ratio** - "the ratio between the % reflectance value at 700nm to that at 400
    nm" - "provides the numerical description of the general slope of VIS", and it is the
    standard quantitative description of a sediment's colour. The paper's own finding: "the
    Q7/4 ratio (from **1.5 to 6**)" across the sediments it measured, and near 1 for the
    greyest ("L* close to 90 deg, Q7/4 ratio close to 1" for the one fjord point "completely
    separated from the general trend").
  - The **mechanism**: "the origin of the colour in the iron oxides has been studied. The
    absorption bands exhibited by the iron oxides from UV to near IR wavelengths originate
    from electronic transitions", and the consequence: "goethite is **yellow** while hematite
    is **red** because the corresponding band lies at a lower wavelength in the visible".
  - The **poles** of the diagram - "Clayey deposits, organic rich deposits (chlorophyll a and
    by products), altered organic matter deposits, **iron rich deposits**, carbonated deposits"
    - and the "Iron-rich deposits" pole is labelled by "a high peak around 575 nm corresponding
    to **hematite**".
  - The purpose: "Colour is a fundamental property of sediment and is often used for
    **lithographic description** to determine sedimentological structures, facies etc."
- **Shahrokhi, S., Babek, O., Mansurbeg, H., Segvic, B., Martin-Martin, J., Kapusta, J., Gregar,
  F., Korenek, M. (2025)**, *Early Diagenetic Controls on Color Variation in Eocene Red Beds: A
  Case Study from the Gercus Formation, Zagros Basin, Kurdistan Region, Iraq*, Journal of
  Sedimentary Research, DOI `10.2110/jsr.2025.005` (the record and the abstract were read; **the
  PDF was not obtained**). Used: "the spectrum of sediment hues, from **red, to yellow-brown,
  gray-green, and gray**, arise from the formation and distribution of **Fe oxy/hydroxides**" -
  so a succession's beds span a *spectrum* of hues, and the spectrum's axis is the iron.
- **Tansem, K., Storemyr, P. (2020)**, *Red-coated rocks on the seashore...*, Geomorphology 395,
  DOI `10.1002/gea.21832` (the record and the abstract were read; the PDF was not obtained).
  Used from iteration 38: "rocks with **red iron films, rocks that are now generally gray**".
- **The crate's own `rock-palette.md`** (iteration 38): the palette, the convex shares, the
  score, and the discipline - "the *mean* colour and the *mean* brightness stay where they
  were (a ground that changes colour changes the game's lighting)". The bed's palette is that
  note's construction at the bed's scale, and it inherits its discipline.
- **The crate's own `bedding.md`** (iteration 36): the beds stack, the bed's tone is a draw on
  its index, and the whole trick - "the bed's *edge* is band-limited away with the distance
  while the bed's *tone* is not, which is the whole trick: at a distance the cliff shows its
  succession and not its stripes". The hole in it: the bed's *tone* is a **scalar**.
- **Andersson, J. (2007)**, *Terrain rendering in Frostbite using procedural shader splatting*,
  SIGGRAPH 2007 *Advances* ch. 5 (corpus `10.1145_1281500.1281668`): the **Overlay** blend,
  which "doesn't affect areas where base mask is 0.0 or 1.0" - the instrument for a modulation
  that must not move the mean.

### The critique and the verdicts (`/tmp/i39/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The region's field was normalised by the noise's rms and multiplied by 0.42, so it **saturated** the selection: one tone took a whole massif, the frame's mean moved **66 DN**, and the massif came out a **flat dark blue silhouette** | NOT YET |
| 2 | The region's bias is **weak** - a bias and not a replacement - and the field's value is the noise's own range, not its rms | NOT YET |
| 3 | The bed's colour was mixed in at *every* distance, so the near face changed in **83 %** of its pixels: the band-limit has to be *inside* the bed's own value, against the fragments' own limit | NOT YET |
| 4 | The bed's colour is band-limited against the **fragments**, the region is a weak bias, and the mean and the near face are untouched | **GREAT** |

| Feature | Verdict |
|---|---|
| The massif at 120 m | **GREAT** (the massifs separate into warm buff and ochre beside cool blue-grey) |
| The massif at 300 m | **GREAT** (the resultant falls 0.759 -> 0.683, the spread rises 28.3 -> 35.4) |
| The face at 3 m | **GREAT** (0.64 % of the pixels differ and the mean is identical) |
| The face at 30 m | **GREAT** (13 % differ, the mean identical) |
| The meadow | **GREAT** (9 of 921 600 pixels, by 1 DN) |
| The cost | see the measurements |
| The effect at 300 m | NOT YET (modest; the chroma does not rise, because at 300 m the rock is 70 % haze and the haze's own blue is the colour the eye reads - the atmosphere's limit, not the palette's) |
| The massifs' *identities* | NOT YET (cosmetic; two adjacent massifs often differ by only a little) |

**The change as a whole: GREAT.** The range is made of different rock, the mean holds, the
near field is untouched, and the meadow is untouched.

### Measurements

- **The distant massif is not one colour.** The hue's resultant over the massif's rock at
  **300 m**: **0.759 -> 0.683**, and the hue's spread **28.3 -> 35.4 degrees**; the hue's 5th
  percentile moves from **113 to 30 degrees**, so the warm end appears. At **120 m**: the
  resultant **0.672 -> 0.596** and the spread **36.8 -> 43.3 degrees**.
- **The mean does not move.** The massif's mean RGB at 300 m: **182.0/198.3/214.3 before and
  after** - identical. At 120 m: 156.3/172.0/186.2 -> **156.3/171.9/185.9**, a 0.3 DN shift.
  The palette's equal-weight mean is exactly (1, 1, 1) and the bed's *value* spread is
  unchanged, so the mean holds by construction.
- **The near face does not change.** The face at 3 m: **0.64 %** of the pixels differ and the
  mean is **identical** (122.9/122.1/122.6). The face at 30 m: 13 % differ, the mean
  identical.
- **The meadow is untouched.** The `plains` preset at 6 m: **9 of 921 600 pixels differ, by 1
  DN**.
- **Cost.** `bench_elements`, High, 1920 x 1080, `mountains`, the **minimum over the
  diagnostic history**. The card was contended for most of the session - a *third* process was
  intermittently using it, and most readings were 0.56-0.70 ms for **both** builds - so the
  paired comparison is not usable, and the cost was measured **directly** instead: the same
  build, the same session, the same card, with the bed's colour **toggled** in the shader.
  That comparison is immune to the card's state, because both halves met it.
  - the bed's colour **off**: 0.631, 0.629, 0.678, **0.612** ms
  - the bed's colour **on**: 0.668, 0.644, 0.626, **0.626** ms

  The minimum over the four is **0.612 ms off and 0.626 ms on, a 2.3 % regression** - inside
  the note's 10 %. The cost is one `perlin3_d` for the region's field, plus a hash, a
  `smoothstep` and four multiplies. The region is evaluated for every rock pixel, and the
  field is at the massif's scale, so it is a *smooth* field and the terrain's own octave
  could carry it in time - but the opaque pass is the crate's hottest path and the region
  must be sampled at the 3D point, so it is its own evaluation.
- **The full gate.** `cargo test` 273 plus the 2 new bed tests, clippy, doc, wasm, the GPU
  integration suite (22 files), the lib GPU modules clouds (10), terrain (8), cover (8) and dust
  (6), and `the_horizon_sky` alone: all pass.

### The next five items, ranked

1. **The blocks' size spread.** Real and visible, but one family of size: a cliff also carries
   the small chips and the big slabs in the same wall. The second generation of blocks was
   tried and rejected (a speckle), so the spread comes from the cells' unequal sizes alone.
2. **The cost of the blocks**, +28 % on the rock's pass in iteration 35, and the two warps and
   the two palettes on top of it. A smaller cell is now in, so the *number* of cells went up;
   the prune's bound and the band-limit are the two levers left.
3. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too - the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
4. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain`
   leak at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.
5. **A game's own light does not fall on the crate's weather.** A lamp lights the town, but
   the crate's rain and snow are lit by the crate's lights alone, so a lamp in a downpour
   lights the *ground* and not the *rain*. A hook for a game's own lights in the precipitation
   and cloud passes would close it; the ownership map keeps the geometry in `bevy_flora`, but
   the lighting hook is this crate's.

## Iteration 40 - 2026-10-22 - a cliff carries slabs and chips: a second joint set, and a spacing that varies

**Commits:** `d0a9d08` (the research note `docs/research/block-sizes.md`), `3502f23` (the
implementation with its test), `76ee3ae` (the note's Deviations), `c9b50b6` (the README, the
changelog and the showcase image), this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The cliff is a field of equal pieces** - a shattered safety-glass window, every cell the same size and shape | 2 | **this iteration** |
| 2 | The cost of the blocks, +28 % on the rock's pass in iteration 35 and the warps and the palettes on top of it | 5 | carried |
| 3 | The dry patch's own texture | 2 | carried |
| 4 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |
| 5 | A game's own light does not fall on the crate's weather | 3 | carried |

Rank 1 beat the carried items because the cliff is the crate's most prominent material and the
*last* thing it was missing: iteration 35 gave it pieces, 36 gave the massif its succession, 37
gave the pieces their irregularity, 38 gave them their colour and 39 gave the colour to the
distance - and through all of that the pieces were all the *same size*. The measurement is
simple and damning: the face at 3 m is a uniform mosaic, and the dusk capture reads as
**cracked mud**.

### What changed

- **A second joint set**: a family of parallel planes across the bedding, and the blocks
  between the two sets are *slabs*. A family of planes is a **period**, and a period is a
  **coordinate** - so the second set costs a dot product, a division and a `fract`, and *no
  second lookup*.
- **The second set's spacing is a *formation's* own draw**, and the formation is 46 m across,
  so the spacing is constant within a formation and changes between formations.
- **The spacing's draw is a Weibull**, the distribution the fracture spacing follows.
- **The planes wander and they end**: a joint set's fractures are non-persistent, and a
  perfectly straight plane set reads as a wire fence across the face.
- The second set's **relief** is along its own normal, so the two sets' steps cross.

### Sources

- **Verdian, J. P., Sklar, L. S., Riebe, C. S., Moore, J. R. (2021)**, *Sediment size on talus
  slopes correlates with fracture spacing on bedrock cliffs*, Earth Surface Dynamics 9:1073-1090,
  DOI `10.5194/esurf-9-1073-2021` (open access; the PDF was downloaded to
  `/tmp/research/verdian2021.pdf` and read in full, 914 lines). Used, and it is the model this
  note adopts:
  - The **mechanism**: the initial size distribution "should depend on three factors: the size
    distribution of discrete blocks latent within bedrock, the weathering processes that modify
    rock in situ, and the weathering processes that detach and mobilize the sediment. Where
    effects of weathering on size are minimal, the initial size distribution should strongly
    reflect the size distribution of latent blocks (i.e., the **"latent size distribution"**),
    which is **set by the spacing and orientation of fractures, foliations, and bedding planes**
    (Sklar et al., 2017). These represent preexisting planes of weakness and determine the
    volume and shape of newly created sediment particles".
  - So a block is the **intersection of THREE families of planes**, and its size is the product
    of three spacings - and a product of three is a much heavier-tailed distribution than one.
  - The **spacings'** own distribution: the fracture spacing's is a **Weibull** ("the Weibull
    form of the exponential distribution ... yielded the best fit to the data in most cases"),
    running "from 2 to 5000 mm" with the site medians "from 10 to 390 mm" and a **geometric
    standard deviation** of 0.214 to 0.551 across the fifteen sites.
  - The **shape**: the blocks are rods, slabs and equi-sided blocks, with the b : a and c : a
    ratios of a site's talus averaging 0.64 and 0.35 for the granodiorite and 0.57 and 0.26 for
    the metasediment, and the universal values for mechanically weathered blocks being **0.67
    and 0.45** (Domokos et al. 2015). A block is **flat and elongated**, and not a cube.
  - The **caveat**: the lower tails "follow a steeper trend than the rest of the data ...
    potentially reflecting an **undersampling of the smallest fractures**" - a survey misses the
    smallest, so a real cliff has *more* small pieces than a measured one.
- **Strauhal, T., Zangerl, C. (2021)**, *The Impact of Fracture Persistence and Intact Rock
  Bridge Failure on the In Situ Block Area Distribution*, Applied Sciences 11, DOI
  `10.3390/app11093973` (the record and the abstract were read; **the PDF was not obtained** -
  MDPI returns 403). Used: "The **in situ block size distribution** is an essential
  characteristic of fractured rock masses"; and the simplification the crate made: "The block
  size distribution can be estimated rather easily for **fully persistent fractures**, but it is
  a challenge to determine this parameter when **non-persistent fractures** in a rock mass
  should be considered. In many approaches, the block size distribution is estimated by
  **assuming that the fractures are fully persistent**, resulting in an underestimation of the
  block sizes for many fracture geometries."
  So the crate's single, always-persistent cell network is exactly the *simplification* this
  paper names - and the simplification is what makes every piece the same kind of piece.
- **Macciotta, R., Grapel, C., Skirrow, R. (2020)**, *Fragmented Rockfall Volume Distribution
  from Photogrammetry-Based Structural Mapping and Discrete Fracture Networks*, Applied Sciences
  10, DOI `10.3390/app10196977` (the record and the abstract were read; **the PDF was not
  obtained**). Used: "the **fragmented volume-relative frequency distribution** of rockfalls"
  is what a fragmentation model needs, and "simple **discrete fracture network** realizations
  ... can be used to reliably estimate rock fall block volumes" - the block volumes are the
  *network's* volumes, so the network is where the size distribution has to live.
- **The crate's own `rock-fragments.md`** (iteration 37): the blocks, the warp and the unequal
  cells, and the sentence that frames this change - "The size spread comes from the **cells
  themselves**: the network's feature points are drawn *towards* their cell's centre, so the
  cells are unequal ... That is the only place a cell network's cells can differ in size, and it
  costs one multiply." The hole in it: the cells differ in size, but every cell is drawn the
  *same way*, so the *pieces* do not read as different kinds.
- **The crate's own `rock-blocks.md`** (iteration 35): the blocks, the joint and the note's own
  warning - "A Worley cell network cannot make them - it bounds *cells*, so a face reads as a
  giraffe's hide" - and the **joint sets** (La Pointe & Hudson 1985): "a rock breaks along
  *joint sets* - families of parallel planes - so its pieces are aligned, not a random heap".

### The critique and the verdicts (`/tmp/i40/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The lattice's scale was varied by a *field of the point*, so the cells' boundaries folded over each other and the face came out a **moire of concentric rings** | NOT YET |
| 2 | The second set is a **coordinate** and not a second network: a family of planes is a period, and a period is a coordinate - so it costs no lookup at all | NOT YET |
| 3 | The spacing's draw was a *power of a uniform*, which concentrates the values near zero, and the spread came out at 2.2:1 where the Weibull's is 3.2:1 | NOT YET |
| 4 | The planes were **straight and evenly spaced** - a **wire fence** across the face, the lines running unbroken from edge to edge | NOT YET |
| 5 | The planes **wander** and they **end**: a finer field breaks them into segments, and the joint is absent where the segment is not | NOT YET |
| 6 | The pieces' areas at 3 m went *down* (the second set's seams divide the pieces) and the lattice's regularity went down by 30 % | **GREAT** |

| Feature | Verdict |
|---|---|
| The face at dusk | **GREAT** (the lattice's regularity is broken: the edge-orientation resultant 0.312 -> 0.219) |
| The face at 3 m and at 8 m | **GREAT** (the pieces' areas spread at 8 m: the p75/p25 ratio 2.29 -> 3.35) |
| The face at 20 m | **GREAT** |
| The distance at 120 m | **GREAT** (unchanged to four decimals) |
| The meadow | **GREAT** (0 of 921 600 pixels differ) |
| The cost | see the measurements |
| The second set's seams' softness | NOT YET (cosmetic; they read as broad grooves rather than cracks) |
| The lattice's diamonds | NOT YET (cosmetic; faintly visible where the second set's segments are sparse) |
| The near face's mean | NOT YET (it moves 2.3 DN, because the second set's relief is along its own normal) |

**The change as a whole: GREAT.** The cliff carries slabs and chips, the lattice's regularity
is broken, the distance is calm, and the meadow is untouched.

### Measurements

- **The lattice's regularity is broken.** The blocks' **edge-orientation resultant** - the
  circular resultant of the edge angles, which is how strongly the pieces' edges agree on a
  direction - falls at dusk from **0.312 to 0.219**, and the gradient's direction's resultant
  from **0.057 to 0.017**. That is the crate's own measure from iteration 37, and it is the
  measure the defect needed: a lattice of equal cells gives every edge the same direction.
- **The pieces' areas spread.** At 8 m the area's p75/p25 ratio rises from **2.29 to 3.35**; at
  3 m it *falls* from 3.19 to 2.68, because the second set's seams *divide* the pieces rather
  than making them bigger. The measure that improved is the *regularity*, and the log says so
  rather than quoting the one that did not.
- **The distance is calm.** The view from 120 m: the material's own high-pass **4.4185 DN
  before and 4.4184 DN after**, the same to four decimals, and the mean RGB identical.
- **The meadow is untouched.** The `plains` preset at 6 m: **0 of 921 600 pixels differ**.
- **The near face's mean moves 2.3 DN** (122.9/122.1/122.6 -> 120.6/120.1/120.8), because the
  second set's relief is along its own normal and the face's shading changes where the two
  sets' steps cross. The far field and the meadow are untouched, so the move is confined to the
  near rock.
- **Cost.** `bench_elements`, High, 1920 x 1080, `mountains`, the **minimum over the
  diagnostic history**. The card was contended for most of the session - a *third* process was
  intermittently using it, and most readings were 0.55-0.85 ms for **both** halves - so the
  cost was measured by **toggling the second joint set** in the shader, with the two halves
  **alternating** and each **rebuilt**, so both met the same card. The comparison is immune to
  the card's state, because both halves met it:
  - the second joint set **off**: 0.281, 0.672, **0.259** ms
  - the second joint set **on**: **0.274**, 0.260, 0.719 ms

  The minimum over the three is **0.259 ms off and 0.260 ms on, a 0.4 % regression** - the
  second joint set is nearly free, which is what the construction says: a family of planes is a
  **period**, and a period is a **coordinate**, so the set costs a dot product, a division and
  a `fract` and *no second lookup*. The only new field is the segment's, and it is a single
  `perlin3_d` on the rock's path.
- **The full gate.** `cargo test` 274 plus the new spacing test, clippy, doc, wasm, the GPU
  integration suite (22 files), the lib GPU modules clouds (10), terrain (8), cover (8) and dust
  (6), and `the_horizon_sky` alone: all pass.

### The new test, and what it measures

`the_second_joint_sets_spacing_varies` measures the second set's **spacing** over many
formations. It **fails on the baseline** - a crate with no second joint set has one spacing for
the whole cliff, and the ratio is 1.00 - and it passes on this one at 3.2:1. The test reads the
spacing from the mirror's *own* function, so the mirror and the test cannot diverge, and the
mirror and the shader are checked against each other by the capture.

### The next five items, ranked

1. **The cost of the rock's material.** +28 % on the rock's pass in iteration 35, and the two
   warps, the two palettes and the second joint set on top of it. The prune's bound and the
   band-limit are the two levers left, and the second set's own segment field is a third.
2. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too - the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
3. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain`
   leak at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.
4. **A game's own light does not fall on the crate's weather.** A lamp lights the town, but
   the crate's rain and snow are lit by the crate's lights alone, so a lamp in a downpour
   lights the *ground* and not the *rain*. A hook for a game's own lights in the precipitation
   and cloud passes would close it; the ownership map keeps the geometry in `bevy_flora`, but
   the lighting hook is this crate's.
5. **The near face's mean moves with every change to the rock's material.** Iteration 40's
   second joint set moved it 2.3 DN, and the crate's rule is that the mean stays where it was.
   The rule needs a *test* that holds the whole rock's mean, so that the next change to the
   material cannot move it silently.

## Iteration 41 - 2026-10-22 - the rock's material is half the frame: band-limit the field, not the feature

**Commits:** `0166e3f` (the research note `docs/research/rock-cost.md`), `676a317` (the
implementation with its tests), this log. **Sibling repos:** none. **Not released.**

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The cost of the rock's material** - it is roughly half the mountains' opaque pass, and the crate's notes have warned about it since iteration 29 | 5 | **this iteration** |
| 2 | The dry patch's own texture | 2 | carried |
| 3 | The overcast dome is flat (`sky_gain` leaks at grazing, diagnosed in iteration 28) | 1 | carried |
| 4 | A game's own light does not fall on the crate's weather | 3 | carried |
| 5 | The near face's mean moves with every change to the rock's material | 2 | carried (the rule needs a test) |

Rank 1 beat the carried items because the rock is the crate's most prominent material and it is
**half the frame's cost**: the mountains' opaque pass is 0.72-0.79 ms with the rock's material
and 0.13-0.38 ms without it. Every other item is a *beauty* gap in one domain; this one is the
price of every frame the game draws, and it has been carried since iteration 29 without anyone
measuring *where* the cost is.

### What changed

- **The bed's region** - the slow field that biases the bed's colour - is only evaluated where
  the bed's colour is wanted. The bed's colour is band-limited against the *fragments*, so
  where a fragment resolves the region was fetched and thrown away.
- **The second set's segment field** - the field that breaks the second joint set's planes into
  segments - is only evaluated where the point is near a plane. The planes are non-persistent,
  so the joint is *absent* over most of the face.
- **`perlin3` is now the value-only noise.** `perlin3_d` computes the value *and* the
  derivative, and the derivative is a second large polynomial. The crate makes five
  evaluations per rock pixel and four of them use the value alone.
- A test that the value-only noise and the derivative-carrying one agree **exactly**.

### Sources

- **Lagae, A., Lefebvre, S., Cook, R., DeRose, T., Drettakis, G., Ebert, D., Lewis, J. P.,
  Perlin, K., Zwicker, M. (2010)**, *A Survey of Procedural Noise Functions*, Computer Graphics
  Forum 29(6), DOI `10.1111/j.1467-8659.2010.01827.x` (in the corpus, read). Used:
  - "Since 1985, compute speed has increased much faster than memory bandwidth. In a sense, we
    can now consider that **cycles are free**, in reference to the fact that most programs in
    today's architectures spend a large amount of their time waiting for cache misses and other
    kinds of memory access" - the survey's framing of *why* the noise's count is the thing to
    measure, and it is the framing of this note: a field fetched where it cannot change the
    answer is pure waste.
  - "**90% of 3D rendering time is spent in shading**, and much of that time is spent computing
    **Perlin noise**" - the survey's own headline for why the noise's cost is a first-class
    problem and not an implementation detail.
  - The survey's speed comparison: "sparse convolution noises, wavelet noise and better
    gradient noise are generally **slower than Perlin noise**, whereas anisotropic noise is
    generally faster" - so the crate's Perlin is the *right* choice and the *count* is the thing
    to cut, and not the function.
- **The crate's own `rock-material.md`** (iteration 29): the rule that every octave is band-
  limited by its own feature size, and the warning that "every octave is a noise evaluation in
  the crate's hottest pass - the mountains' opaque pass is 0.138 ms on the baseline build", with
  the measured consequence: "The 3D noise is twice the 2D one's per octave, and the mountains'
  opaque pass is the crate's hot path: **four octaves took it from 0.136 ms to 0.249 ms**. One
  octave leaves it at [0.144 ms]". So the crate has always known that a *count* of octaves is
  the cost, and the material has grown from two octaves to **five** `perlin3_d` and a cell
  network since.
- **The crate's own `rock-blocks.md`** (iteration 35): the band-limit's own rule - "The
  band-limit is *known before the network is looked up*, and the lookup is the expensive part
  - 27 cells, each a hash and a distance. So where the blocks cannot resolve, they are not
  looked up at all: the far terrain pays nothing for them, and the mountains' opaque pass does
  not carry the cost over the whole massif." The *pattern* of this note is that note's, applied
  to the fields the block's note did not know about.
- **The crate's own `rock-fragments.md`** (iteration 37): the warp's own band-limit - "a warp
  that varies faster than the cell it moves **scrambles** the lattice instead of displacing
  it", and the scale's rule that the warp must be *coherent* over many cells.
- **The crate's own `rock-palette.md`** (iteration 38): the **rind**, the film of iron and
  manganese on the surface, which is a *fine* feature - "a coating 100 to 500 um thick" - and so
  is band-limited away with the distance.
- **The crate's own `bed-colour.md`** (iteration 39): the **region**, the slow field that biases
  the bed's selection so a whole massif leans towards one rock, and the *bed's colour*'s own
  band-limit - the bed's colour is mixed in only where the *fragments* do not resolve.

### The critique and the verdicts (`/tmp/i41/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | The region's gate **multiplied the field by the keep**, and the keep is between zero and one where the two band-limits cross - so the bias was weakened there and **13 % of the near face changed** | NOT YET |
| 2 | The gate must skip the **evaluation** and not the field's *value* | NOT YET |
| 3 | The segment's gate cut the joint's **smoothstep tail**, and again **13 % of the near face changed** | NOT YET |
| 4 | The gate covers the joint's **whole extent** (`d < 2*soft`), and then the change is 0.34 % - the GPU's own non-determinism | **GREAT** |

| Feature | Verdict |
|---|---|
| The image does not change | **GREAT** (every angle under 0.55 % of its pixels, every mean identical to three decimals) |
| The value-only noise | **GREAT** (bit-identical by test, and crate-wide) |
| The saving | **Not proven.** 3.2 % between two minima of a bimodal reading is inside the card's noise; the honest claim is that the change is *no slower* (corrected in iteration 42) |
| The gates' branch cost | NOT YET (cosmetic; a branch on a *divergent* condition costs more than a straight-line fetch, and the segment's gate is divergent by nature) |
| The material's total | NOT YET (it is still half the pass; the change is a few per cent of a cost the geology's field count dominates) |

**The change as a whole: GREAT on the picture** (the same to the last pixel that can be
compared). The saving is a direction, not a measurement.

### Measurements

- **The image does not change.** Every angle differs from the baseline by under 0.55 % of its
  pixels - the GPU's own non-determinism - and every mean is identical to three decimals: the
  face at 3 m 120.26 -> 120.26, at 12 m 121.95 -> 121.95, at 30 m 140.78 -> 140.78, the dusk
  144.41 -> 144.41, the massif at 120 m 169.56 -> 169.56, and the `plains` preset
  163.77 -> 163.77. The value-only noise is **bit-identical** by test, and the two gates skip
  the *evaluation* and not the field's *value*.
- **Cost.** `bench_elements`, High, 1920 x 1080, `mountains`, the **minimum over the
  diagnostic history**, the baseline build and this one **interleaved** in one session so both
  meet the same card. Eight trials were scheduled; the run hit its 3000 s timeout after **six
  baseline and five change trials** (corrected in iteration 42: this entry first said "eight
  trials each").
  - the **baseline** (the iteration-40 build): 0.253, 0.741, 0.670, 0.674, 0.721, 0.272 ms
  - **this one**: 0.262, 0.647, 0.664, 0.713, **0.245** ms

  The **minimum over the trials** is **0.253 ms on the baseline and 0.245 ms on this one**. A
  3.2 % gap between two minima of a bimodal reading is inside the noise, so the defensible
  claim is that the change is **no slower**; the field-toggle data (`/tmp/i41/fields.txt`)
  says where the remaining cost is, not that this change removed a measurable part of it.

  **The card's readings are bimodal**, and the crate's own note says why: "GPU timings on a
  lightly loaded NVIDIA card read **5-10x slow** (P8/P5 clocks under Xvfb)". The same build
  reads 0.25 ms or 0.74 ms depending on the card's clocks, and a third process was
  intermittently using the card. So the *minimum over many trials* is the estimator and a
  single reading is not, and the comparison is only sound because the two builds were
  interleaved and both saw the same card. The two lowest readings of the session are the
  change's 0.245 and the baseline's 0.253.
- **The full gate.** `cargo test` 276 lib tests (the 2 new ones included), clippy, doc, wasm,
  the GPU integration suite, the lib GPU modules clouds (10), terrain (8), cover (8) and dust
  (6), and `the_horizon_sky` alone: all pass.

### The next five items, ranked

1. **The rock's material is still half the pass.** The change is a few per cent of a cost the
   *count* of the material's fields dominates: five `perlin3` and a cell network per rock
   pixel. The field count is what the geology needs, so the next lever is the *network* - the
   cell lookup is 27 cells, each a hash and a distance, and the prune's bound is the only
   thing that touches it.
2. **The near face's mean moves with every change to the rock's material.** Iteration 40's
   second joint set moved it 2.3 DN, and the crate's rule is that the mean stays where it was.
   The rule needs a *test* that holds the whole rock's mean, so that the next change to the
   material cannot move it silently.
3. **The dry patch's own texture.** The palette gives the ground a *hue*; a dry patch needs a
   *texture* too - the blades inside it should take the patch's hue rather than the turf's, and
   a dry patch is coarser than green grass. The colour half is in this crate; the geometry half
   (a grass mesh) belongs to `bevy_flora`.
4. **The overcast dome is flat**, with its blocker diagnosed in iteration 28 (the `sky_gain`
   leak at grazing); it carries its Eddington implementation and its measurements in `/tmp/i28`.
5. **A game's own light does not fall on the crate's weather.** A lamp lights the town, but
   the crate's rain and snow are lit by the crate's lights alone, so a lamp in a downpour
   lights the *ground* and not the *rain*. A hook for a game's own lights in the precipitation
   and cloud passes would close it; the ownership map keeps the geometry in `bevy_flora`, but
   the lighting hook is this crate's.

## Iteration 42 - 2026-10-22 - rocks on the ground: a scatter of boulders and stones, not a painted texture

**Commits:** `3402ca6` (iteration 41's record corrected), `f2be650` (the research note `docs/research/rock-scatter.md`), `baee56c` (the implementation with its tests), `761275f` (the README section and showcase `docs/rocks.png`, the changelog and the note's deviations), this log. **Sibling repos:** none. **Not released.**

The user's focus for this run: "better looking terrain, more variety, scattering rocks and debris, stuff to make the ground look more realistic", with stylistic variants of everything to follow.

### Iteration 41's record, corrected first

Iteration 41's log said its benchmark ran "eight trials each"; the run hit its 3000 s timeout after six baseline and five change trials, and a 3 % gap between two minima of a bimodal reading is inside the card's noise. The log and the changelog now say so, and the saving is "no slower", not "3.2 % faster" (`3402ca6`).

### Candidates

| Rank | Candidate | Class | Decision |
|---|---|---|---|
| 1 | **The ground is an empty carpet**: nothing on a meadow, a hillside or below a cliff has a silhouette or a shadow; every stone is painted into the terrain's material (`/tmp/i42/before/hills_eye.png`, `hills_ground.png`, `mtn_foot.png`) | 2 (the user's focus) | **this iteration** |
| 2 | The cliff's joints read as a web of flat grey ribbons, chicken wire over every near cliff (`/tmp/i42/crop_web.png`; also in iteration 40's showcase `docs/block_sizes.png`) | 1 | next |
| 3 | Saturated blue-black lens pits on the cliff's shaded fragment flanks at dusk (`/tmp/i42/crop_pits.png`) | 1 | carried |
| 4 | The world ends at 1 km in aerial views: a white plane beyond the 2 km terrain (`hills_aerial.png`) | 1 | carried |
| 5 | Stylistic variants of the crate's materials (the user's next area) | 3 | carried |

Rank 1 beat the class-1 web because the user named the scatter as the focus, and because the rocks are their own material, so they neither depend on nor hide the web; the web is ranked first for the next iteration.

### What changed

- **`RocksPlugin`** (in `ElementsPlugins`): boulders and stones as meshes over the `Terrain`.
  - **Where**: Golombek's total cover `k` per point from the terrain readback - a **talus** in the scree's band of slope (16-40°) *below a cliff* (the steepest of four points 6-36 m up the slope, as rock), angular blocks; the **meadow**: Golombek's plain (1.5 %), up to rocky ground (5 %) in patches 220 m across and on steep hillsides (Verdian: faster erosion, more rock at the surface); both scaled by an 18 m cluster field so rocks gather in heaps; nothing above 40° and nothing on sand (sediment over 0.15-0.5 m).
  - **How big**: Golombek's exponential law, rocks of `D` or more cover `k·exp(−(1.79 + 0.152/k)·D)`; four size classes from 0.25 to 3.6 m, each a jittered lattice whose cell is its footprint and a hashed threshold against the expected count (HZD's density-and-dither placement, on the CPU), the diameter by inverse transform.
  - **What shape**: twelve shapes, a block cut by six jittered joint planes and chips under a soft minimum with analytic normals; angular (sharp edges, the talus) and rounded (worn, the meadow); squashed to Verdian's talus axis ratios (b:a 0.55-0.85, c:a 0.30-0.55), lying on the largest face, tilted 70 % to the slope, a quarter to a third buried.
  - **Where exactly it sits**: on the surface the CDLOD patches draw - the readback on the covering patch's lattice (its own triangles), blended to the next level by the vertex shader's morph factor; each rock caches its two lattice heights and morphs between them as the camera moves.
  - **How far**: to `ElementsQuality::rock_range() · D` (100 m per metre at High), shrinking to nothing over the last fifth; a near and a far mesh (1280 and 320 triangles) switch at 22·D.
  - **Material**: `WeatheredMaterial` with the terrain rock's albedo, the palette's four tones at 60 % per shape, a darkened base, and a **stone grain**.
- **`WeatheredExtension::with_grain(feature_m)`**: a mottle at the grain's size and a speckle at an eighth, two octaves each of the terrain's band-limited 3D noise, on the albedo and the shading normal.
- **`TerrainSnapshot::sediment`** (breaking for struct literals): the readback copies the sediment beside the surface.
- `ElementsQuality::rock_range`; `RockSettings`, `ScatteredRock`, `RockStats`.
- `terrain_tour`: `ELEMENTS_FLY`; `bench_elements`: `ELEMENTS_EYE`/`LOOK`/`AGL`, `ELEMENTS_ROCKS=off`, `per_view_shadow_pass` reported; `vec3_env` moved to `examples/common`.
- `AGENTS.md`: the trap that a crossfading `VisibilityRange` leaves black holes under the depth prepass.

### Sources

- **Golombek, M. P. et al. (2021)**, *Rock size-frequency distributions at the InSight landing site, Mars*, Earth and Space Science 8, DOI `10.1029/2021EA001959` (the Caltech authors repository's PDF, read): the exponential model `Fk(D) = k exp[−q(k) D]`, `q(k) = 1.79 + 0.152/k`; abundances of 1-2 % on plains and 4-5 % on rocky ground; "the usual hemisphere of one half the diameter". Golombek & Rapp 1997 not obtained (Wiley 403).
- **Verdian, J. P. et al. (2021)**, Earth Surface Dynamics 9:1073, DOI `10.5194/esurf-9-1073-2021`: talus b:a and c:a 0.57-0.64 and 0.26-0.36, Domokos's 0.67 and 0.45; "bedrock exposures (and thus areas with TR ∼ 0) are patchy"; "faster erosion should generally lead to larger initial particle sizes".
- **Galin, E. et al. (2019)**, *A Review of Digital Terrain Modeling*, CGF 38(2), DOI `10.1111/cgf.13657` (corpus): talus angle 30-45°, accretion cones "at the bottom of cliffs", spheroidal weathering rounds edges first.
- **van Muijden, J. (2017)**, *GPU-Based Procedural Placement in Horizon Zero Dawn*, GDC 2017 (Guerrilla's slides PDF): density maps, a dither pattern with spacing = footprint, range and threshold tests; ~250 µs for 100 000+ objects; per-rock colour variance maps.
- The crate's own `scree.md`, `rock-palette.md`, `rock-material.md`, `terrain.md`, `wetness.md`.

### The critique and the verdicts (`/tmp/i42/critique.md`)

| Round | Finding | Verdict |
|---|---|---|
| 1 | No rocks on the hills (a 0.4 % floor gives `q = 39.8`: rocks of 25 cm and up cover 2·10⁻⁷); black dithered checkerboards on every fading rock; potatoes | FAIL |
| 2 | Mask did not fix the black (shadows off: same; abrupt ranges: gone); shapes still loaves; basalt rocks painted blue | FAIL |
| 3-5 | Shrink fade (no black); polyhedral blocks; tone at 60 %; mottle + speckle grain (the plan's fBm showed only smooth bands at 5×) | NOT YET |
| 6 | A dense ribbon of talus along a grass flank with no cliff (a dry-stone wall); even "chocolate chip" spacing on the aprons | NOT YET |
| 7-8 | Talus only below a cliff; clusters on talus and meadow | GREAT (talus, shapes, grain, seat, fade, rules, snow) |

| Feature | Verdict |
|---|---|
| Talus aprons below cliffs | GREAT |
| Shapes | GREAT |
| Stone grain | GREAT |
| Seat, burial, contact shadows | GREAT |
| No pop (glide) | GREAT |
| Placement rules (cliffs, sand, dunes) | GREAT |
| Snow on the tops | GREAT |
| Rain on rocks | NOT YET (cosmetic: the weathered material's own wet response darkens them but adds little gloss; unchanged by this iteration) |
| The flat meadow | correct and sparse (Golombek's 1.5 % plain); a game wanting a stonier pasture raises `meadow_cover` |
| The cliff's joint web | FAIL (pre-existing, iteration 40; rank 1 next) |

### Measurements

- **The rocks are there.** At High the mountains' rings hold about **6 000 rocks** around a camera in the talus (5 900–6 300 through the glide, counted by the seat pass with the cliff rule in place); the hills' flat meadow a few dozen in view, a rocky patch dozens at eye level (`/tmp/i42/r8/patch_eye.png`). Dunes: none.
- **The law holds through the placement** (`the_scatter_reproduces_the_cumulative_area`): over a 0.6 km² patch at `k = 0.30`, rocks of 1 m and up cover the law's `F(1) − F(3.6)` within 25 %.
- **Placement rules** (`no_rock_on_the_cliff_or_the_sand_and_the_talus_is_dense`, `a_talus_lies_below_a_cliff_and_nowhere_else`): no rock on a 45° face, on 0.8 m of sand or on a sand flat; below a 60° cliff the 26° apron is angular blocks and at least 5× as dense per metre as the same slope 70 m further down (rounded); a uniform 26° hillside with no cliff above holds no talus.
- **Not a lattice** (`the_scatter_is_not_a_lattice`): Clark–Evans ratio between 0.85 and 1.6. **Deterministic** (`a_tile_built_twice_holds_the_same_rocks`). **Shapes** (`the_shapes_are_slabs_and_blocks_not_spheres`, `the_meshes_are_closed_and_face_out`, `the_angular_family_has_flat_faces_and_the_rounded_one_does_not`, `the_palette_is_the_rocks_mean`). **Seat** (`the_seat_is_the_surface_the_patches_draw`): level 0 is the readback at the nodes, a quad is its triangles, a fully morphed level is the next one.
- **GPU cost.** `bench_elements`, High, 1920 × 1080, `mountains`, the camera in the densest talus (`ELEMENTS_EYE=45,0,52 ELEMENTS_LOOK=10,0,30 ELEMENTS_AGL=1.7`), rocks on and `ELEMENTS_ROCKS=off` interleaved, five trials each, the minimum over the diagnostic history:

  | Pass | Rocks on | Rocks off | Δ |
  |---|---|---|---|
  | `main_opaque_pass_3d` | 1.050 ms | 0.938 ms | +0.112 ms |
  | `early prepass` | 0.048 ms | 0.041 ms | +0.007 ms |
  | `per_view_shadow_pass` | 0.071 ms | 0.066 ms | +0.005 ms |

  **About +0.12 ms**, under the note's 0.3 ms budget. The clocks were logged in the same script: the card sat in **P5 (median 660 MHz) for 1433 of 1501 samples** and reached P2 for 38, so these are upper bounds; a 3840 × 2160 run stayed in P5 too (451 of 601 samples, Xvfb's presentation is the bottleneck) and never left the bench's warm-up in 150 s.
- **CPU cost.** A glide at 8 m/s through the talus (`ELEMENTS_FLY=8`, 600 frames, the first 60 dropped): the placement (`scatter`) mean **0.033 ms**, p95 0.06 ms, max 1.0 ms (a frame that builds tiles); the seat (`seat_rocks`) mean **0.134 ms**, p50 0.114 ms, p95 1.02 ms, max 1.21 ms - the p95 is the full re-read when the terrain's surface version moves (hydrology steps it). Before each rock cached its two lattice heights the seat read 0.425 ms mean and 0.649 ms p50; the last frame of the glide is the same picture either way (0.74 of 921 600 pixels differ).
- **The full gate.** `cargo test` 327 (the rocks' 11 and the snapshot's sediment check new), clippy, doc, wasm, the WESL check, the GPU integration suite (57 passed), the lib GPU modules clouds (10), terrain (8), cover (8) and dust (6), and `the_horizon_sky` alone: all pass.

### The next five items, ranked

1. **The cliff's joints read as chicken wire.** Every near cliff wears a web of flat, constant-width grey ribbons crossing each other (`/tmp/i42/crop_web.png`, `mtn_foot`, `mtn_dusk`, and iteration 40's showcase). A joint is a thin dark shadowed step, not a grey band laid over the rock. Class 1, and now beside real rock meshes it is the worst thing in a mountain frame.
2. **The painted scree reads as polka dots beside real stones.** The terrain material's scree (iteration 34) paints round domes on the apron; with mesh rocks on it they read flat. Fade the painted stones where the mesh talus is dense, or reshape them.
3. **Stylistic variants** (the user's next area): a style knob over the crate's materials - the rock palette, the turf palette, the rocks' grain and tone, the sky's grading - starting with the ground.
4. **The world ends at 1 km in aerial views** (`hills_aerial.png`): a white plane beyond the terrain's edge.
5. **Blue-black pits on the cliff's shaded fragments at dusk** (`crop_pits.png`).

## Iteration 43 - 2026-10-22 - the cliff's joints: five rounds, NOT YET, not shipped

**Commits:** `d0adc32` (the research note `docs/research/rock-joints.md`), the note's outcome, this log. The attempt is preserved on the branch `attempt/iter-43-joints` (`7bc5e14`), **not merged**: `main`'s code is iteration 42's. **Sibling repos:** none. **Not released.**

### Candidates

1. **The cliff's joints read as chicken wire** (class 1, blocker): every near cliff is a web of flat grey ribbons and a fence of straight bands (`/tmp/i43/crop_face3.png`, `crop_face30.png`). Four causes in `rock_block_scale`: the first set's "joint" was the distance to the *lattice cell's wall*, not to the Voronoi border; it was up to 0.4 of a half-cell wide, and `soft = max(width, footprint)` widened it with distance at full darkness; the relief steps were 0.95 slopes over twice that; and the second set's step used the unwandered, unsegmented plane, while its darkening waved by 13 %.
2. The painted scree reads as polka dots beside the mesh rocks.
3. Stylistic variants (the user's next area).
4. The world ends at 1 km in aerial views.
5. Blue-black pits on the cliff's shaded fragments at dusk.

The joints beat the scree because they are on every cliff at every distance and in the user's focus; the scree shows only on aprons.

### What was tried (`/tmp/i43/critique.md`; pairs and 3× zooms in `/tmp/i43/pairs_after{,2,3,4,5}/`)

| Round | Change | Verdict, and what the face read as |
|---|---|---|
| 1 | `cell3` returns the border (bisector of the two nearest points); hairline traces 1.2–3 cm anti-aliased by coverage; a facet per piece; the second set planar (wander 0.04), split per block | NOT YET: the chicken wire is gone, but the face is crazy paving — convex tiles of one size, each a flat palette tone, each outlined |
| 2 | The tabular stretch restored (below) | NOT YET: a dry-stone wall; a patio where the slope parallels the bedding |
| 3 | Open and tight joints: cells grouped 2.6 cells across, a tight joint at 0.35 of the ink; the palette per group | NOT YET: camouflage, the hue jumping block to block |
| 4 | The bed's palette at every distance, the blocks carrying value only | NOT YET, the clearest step: at 12 m, at dusk and under overcast the cliff reads as a banded succession of different rock; inside each bed a cobble mosaic of per-piece facets |
| 5 | A facet per open block, a 0.08 crease per piece, value ±14 % and ±4 % | NOT YET: the groups' rounded, stair-stepped outlines read as camouflage blobs |

Every round kept its invariants: the plains byte-identical (or within GPU noise), the far massif (`far120`) 0.4–0.6 % of pixels changed with the same mean, no `failed to process`. After the fifth failed round the skill's rule applied: stop, log, and keep the last GREAT state. No benchmark was run, since no round reached GREAT.

**Best hypothesis.** A jointed face reads as rock when its *open* joints are persistent **plane sets** — bedding partings and one or two cross sets with Weibull spacing, the cross joints terminating at the partings and staggered between them — so the blocks are parallelepipeds with straight edges. A Voronoi network, whole (paving) or grouped (camouflage), cannot give a block a straight edge; it should carry only the tight hairlines. Keep from the attempt: the coverage-anti-aliased trace, the bed's colour at every distance, the tabular fix and the exact prune bound (`docs/research/rock-joints.md`, `## Outcome`).

### Defects found on `main` (open; each changes the image, so they wait for the next attempt)

- **The blocks are columns, not slabs.** `0e58b68` turned the bedding stretch `p + n(p·n)(T − 1)` into `p − n(p·n)(1 − 1/T)`, against `rock-blocks.md` Deviation 10, on a test that compared two directions both lying in the bedding plane. Counting blocks along real cuts, `main` crosses **0.55 blocks per metre across the beds against 1.42 along them** (2.5 m columns). The attempt's rewritten `the_blocks_are_tabular` fails on it and passes on the multiply.
- **`cell3`'s prune bound is not exact, and the mirror's is a different bound.** The WESL's `max(o − f, f − o − 1)` is not clamped at zero, so on an axis where the neighbour spans the point its square inflates the bound and a cell holding a nearer point can be pruned; the CPU mirror's `abs().max()` is the cell's *far* corner. The attempt clamps both, and its exhaustive test (343 cells, 20 000 points) finds the nearest point always exact and the border exact within the widest bevel's reach; the 27-cell search misses a second point on 1 point in 4 000, 7.5 cm deep in its cell.

### Sources

- **Palmström, A. (2015)**, *Joint characteristics*, RockMass.net (non-DOI; read in full): separation "mainly less than a millimetre"; planar waviness u < 1 %, wavy > 2 %; "continuous joints terminate at other joints".
- **Worley, S. (1996)**, *A Cellular Texture Basis Function*, SIGGRAPH '96, DOI `10.1145/237170.237267` (PDF read, not ingested): `F2 − F1` is zero on the Voronoi boundary; a value constant per cell with a bump at the boundary is flagstone.
- **Lagae, A. et al. (2010)**, *A Survey of Procedural Noise Functions*, CGF 29(8), DOI `10.1111/j.1467-8659.2010.01827.x` (corpus).
- **Verdian, J. P. et al. (2021)**, Earth Surface Dynamics 9:1073: the latent block size is set by fracture, foliation and bedding spacings (the plane-set hypothesis).

### Measurements (the attempt, CPU mirror)

- Over a 30 m oblique cut: round 1, 41 pieces, 5.6 % of the cut dark, every piece's edge traced and every trace at an edge; round 4, 93 pieces in 55 open blocks, 5.9 % dark, the same two properties.
- The cross joints' worst waviness over 175 four-metre segments: **1.24 %** (Palmström's planar limit 2 %).
- A trace's integral across the line is the same within 2 % from a 1 mm to a 1 m footprint; at a footprint eight times the half-width its peak is 0.25.
- The lib suite at round 1: 290 passed.

### The next five items, ranked

1. **The cliff's joints, a second attempt: blocks cut by plane sets.** Start from `attempt/iter-43-joints`: keep the traces, the bed's colour at every distance and the two fixes above; replace the open joints with bedding partings and staggered cross joints (the hypothesis), keeping Voronoi for tight hairlines only. Still the worst thing in a mountain frame.
2. **The painted scree reads as polka dots beside real stones.**
3. **Stylistic variants** (the user's next area): a style knob over the crate's materials, starting with the ground.
4. **The world ends at 1 km in aerial views.**
5. **Blue-black pits on the cliff's shaded fragments at dusk** (in the baseline and every round of the attempt, `dusk.png` lower left).

## Iteration 44 - 2026-10-22 - the cliff cut by joint sets: five rounds, NOT YET, not shipped

**Commits:** `c860457` (the research note `docs/research/joint-sets.md`), the note's deviations and outcome with the capture trap in `AGENTS.md`, this log. The attempt is preserved on the branch `attempt/iter-44-joint-sets` (`c301388`), **not merged**: `main`'s code is iteration 42's. **Sibling repos:** none. **Not released.**

The user's focus for this run: better-looking terrain, variety, rocks and debris, a realistic ground, stylistic variants; added mid-run, PBR textures in everything, especially the terrain, using golem's texturing.

### Candidates (`/tmp/i44/findings.md`)

1. **The cliff's joints are chicken wire** (class 1, worst in the new `side_ne`), and the mid-distance rock is a scaly speckle (`side_sw`).
2. The painted scree's polka dots beside the mesh rocks (`side_ne`).
3. Stylistic variants.
4. The world ends in a white plane at eye level too (`side_sw`), not only from the air.
5. Blue-black pits at dusk.

The joints beat the scree: every cliff at every distance, against aprons only, and iteration 43's outcome gave a concrete model to try.

### What was tried (`/tmp/i44/critique.md`; pairs and zooms in `/tmp/i44/pairs_after{,2,3,4,5}/`)

The cell network is replaced by the structure a sedimentary cliff is cut by: **bedding partings** split each bed into layers (a bedding style per bed, thin and flaggy to massive); **systematic joints** spaced in proportion to the layer's thickness at a ratio drawn per bed and confined to their layer, so they end at the partings and are staggered; **cross joints** only between two systematic joints, never lining up across one, likelier in narrow intervals. `cell3` is deleted, with its inexact prune and the inverted tabular stretch.

| Round | Change | Verdict, and what the face read as |
|---|---|---|
| 1 | The joint sets, coverage traces, facets, the bed's colour at every distance | NOT YET: bedded rock at last (`mtn_foot`, `face12`, `dusk`, `side_sw`); dashed partings at 3×; masonry in the thin beds |
| 2 | Roughness (0.6 m, 3 cm) on all three sets, aperture along the crack, block value ±5 % | NOT YET: natural cracks; still dashed; a flat painted wall |
| 3 | Two-pixel kernel, ledges (cliff-and-bench) | NOT YET: still dashed — measured at `after3/face3.png` rows 264-267: 117-118 then 75-89, the bevel's slope flipping sign at the crack |
| 4 | Round-floored bevel, face relief, jitter 0.8, deeper ledges | NOT YET: dashes gone (row 263: 108 → 82 → 109); isolated dark rectangular blocks |
| 5 | Facet 0.12 | NOT YET: stratified rock at 3-12 m in direct light; a quilted mottle at 30 m (`face30`); wood grain under overcast |

Every round: `far120` 0.9 % of its pixels and the same mean (the bed fix below moves some band edges), `plains` within GPU noise, no `failed to process`. Five failed rounds, so the code reverts; the note's `## Outcome` holds the hypothesis: a heavy-tailed aperture per joint and parting, a multi-scale band-limited face relief, partings of unequal prominence.

### Defects found on `main` (open)

- **A bed changes up to 0.33 m past its own boundary.** `bed_value`'s inversion corrects an overshot estimate but not an undershot one (`the_partings_split_the_beds_into_layers_of_many_thicknesses` on the attempt failed on 18 boundaries in 200 m before the step-up), and the bed's tone hash uses the uncorrected estimate. The attempt fixes both.
- Iteration 43's two: the inverted tabular stretch and `cell3`'s inexact prune (the attempt deletes `cell3`).

### Sources

- **Bai, T. (2000)**, *Formation and spacing of orthogonal cross joints*, Stanford Rock Fracture Project 11 (`https://stacks.stanford.edu/file/druid:zb158kp7289/RFP_2000_Bai.pdf`, read in full): cross joints abut systematic joints and form a ladder; systematic joints are confined by layer boundaries; S/T > 2 extremely rare; cross-joint spacing ~S or ~T.
- **Bai & Pollard (2000)**, JSG 22:43, DOI `10.1016/S0191-8141(99)00137-6` (abstract): spacing linear in layer thickness, critical ratio ~1.0.
- **Ji et al. (2023)**, JRMGE, DOI `10.1016/j.jrmge.2023.03.012` (abstract; PDF downloaded): cross-joint spacing 1.5 × systematic; s/t 4.3 and 6.4 at low strain, ~1 strained.
- **Akenine-Möller et al. (2018)**, *Real-Time Rendering* 4th ed. (corpus): a perfectly flat brick face looks wrong; bricks shadow the mortar.
- Wikipedia's *Bench (geology)* (after the AGI *Glossary of Geology*) and *Mesa* (after Easterbrook): cliff-and-bench profile of resistant and weak layers.
- Palmström 2015, Verdian et al. 2021, Talling 2001 (earlier notes). Not obtained: Narr & Suppe 1991, Gross 1993 (Elsevier; cited through Bai 2000).

### Measurements

- Mirror (the attempt, 22 tests pass): 295 layers over 200 m, p10 0.28 m, p90 1.21 m; median S/T 1.71, the thickest quarter of layers' spacing 1.93 m against the thinnest's 0.66 m; the joints' mean offset across a parting 0.34 of a spacing (0 if they continued); cross joints in 85 % of narrow intervals and 34 % of wide ones, mean offset across a systematic joint 0.35; a 30 m cut 4.9 % dark, every block edge a trace.
- **Cost** (`bench_elements`, `mountains`, the cliff filling the frame from `ELEMENTS_EYE=0,0,0 ELEMENTS_LOOK=60,0,60 ELEMENTS_AGL=12`, three interleaved trials each, the last per-pass minimum of each run; clocks P5 for 413 of 450 samples, P2 for 24, so upper bounds with ~20 % run-to-run spread): `main_opaque_pass_3d` base 1.490 / 1.517 / 1.774 ms against the attempt's 1.823 / 1.394 / 1.404 ms; `early prepass` 0.076 against 0.082; `per_view_shadow_pass` 0.066 against 0.070. No slower: the joint sets cost no more than the cell network they replace.

### golem's texturing, for the user's PBR direction

This session's golem tools offer no texture or PBR-map generation: `health_check` lists `generate_3d`, `generate_3d_multi_image`, `segment_mesh`, `generate_garment` and `generate_orbit_views` (all unavailable on the GPU now) and `animate_mesh`; `generate_image` (FLUX.2 on bmb) has no tiling or map options. A texturing tool added since would appear only in a new session — check `health_check` and the tool list first.

### The next five items, ranked

1. **PBR texture sets for the terrain** (the user's direction), first slice the **rock**: tileable albedo, normal, roughness and height maps — from golem if a new session offers texturing, else FLUX tiles with derived maps — sampled triplanar in the rock's own frame on the joint-set structure of `attempt/iter-44-joint-sets`, so the blocks' faces carry real surface detail (the attempt's quilted relief and wood grain). Research: texture budget and compression for wasm, WebGPU binding limits beside the terrain's two storage buffers, band-limiting by mip.
2. **The cliff's remaining gaps**, if the textures do not close them: a heavy-tailed aperture per joint and parting, partings of unequal prominence (`joint-sets.md` `## Outcome`); with the bed-inversion fix.
3. **Stylistic variants**: a style knob over the materials, starting with the ground.
4. **The painted scree's polka dots** beside the mesh rocks.
5. **The world ends in a white plane** at eye level (`side_sw`) and from the air.

## Iteration 45 - 2026-10-23 - PBR detail textures for the ground: five rounds, NOT YET, not shipped

**Commits:** `344a012` (the research note `docs/research/ground-textures.md`), the note's deviations and outcome with a silent-miscompile trap in `AGENTS.md`, `wesl-check` checking library modules (shipped: a tooling fix), this log. The attempt is preserved on the branch `attempt/iter-45-ground-textures` (`8d7c1fd`), **not merged**: `main`'s code is iteration 42's. The attempt's packed set (25 MB) is not committed; `uv run tools/ground-textures/prep.py tools/ground-textures/golem.json assets/ground/ground.ktx2` regenerates it. **Sibling repos:** none. **Not released.**

The user's focus for this run: better-looking terrain, variety, rocks and debris, a realistic ground, stylistic variants; PBR textures in everything, especially the terrain; golem.

### Candidates (`/tmp/i45/findings.md`)

1. **The meadow underfoot is a green felt** and the sand a beige blotch (`hills_down`, `hills_eye`, `plains_down`, `dunes_down`); from 12 m the hills read as a low-poly game.
2. The cliff's chicken wire at the talus foot (needs the joint sets and a rock texture).
3. Stylistic variants (a texture set is one).
4. The painted scree's dots.
5. The world's white edge.

The ground beat the rock: it is in every eye-level frame, and a texture could reach GREAT on it alone, where the rock's would sit under the chicken wire.

### What was tried (`/tmp/i45/critique.md`; pairs in `/tmp/i45/{p,q}_*.png`)

`GroundTextures { image, tile_m, grain }`: a 2D array of six layers, for each of sand, soil and grass an albedo **detail** (a multiplier with mean 1, so the landscape's colour does not move and the far mips are no change) with a height, and a normal, roughness detail and occlusion. Hex-tiled (Mikkelsen 2022: three randomly offset and rotated taps on a triangle grid, weights `w⁷` times the tap's height) with explicit gradients; the ground layers blended by their heights (`w · exp(8 (h − ½))`, continuous at the sampling gate); without a set the shader takes the old path bit for bit. The set: three golem FLUX.2 albedos, packed by a new `tools/ground-textures/prep.py` (tileable by a variance-preserving cross-fade, low frequencies divided out, de-lit, height, normal at a chosen mean tilt, roughness and occlusion derived, written as a KTX2 with every mip level).

| Round | Change | Verdict, and what the ground read as |
|---|---|---|
| 1 | The plan, the detail stored at `· 128 / 255` | NOT YET: blades and litter at last; salmon-pink confetti everywhere — 21-29 % of the grass's detail clipped at 2× and lost its hue |
| 2 | Stored at `· 64` (range [0, 4)), de-lit (exponent 0.65) | NOT YET: the confetti subdued; the meadow 26-32 DN darker |
| 3 | The relief as a mean tilt (4°, 10°, 14°; it was 47° on the grass) | NOT YET: still 24 DN darker on the meadow and 21-27 on the dunes |
| 4 | A silent shader miscompile renamed away (below) | NOT YET: means within 0.3 DN from 12 m; leaf confetti on the plains, the dunes smooth paste, the dusk mid field flat |
| 5 | The procedural grain's relief kept whole; a grass source without leaves | NOT YET: real pasture underfoot; the soil's own leaves still confetti on the plains; the dunes plastic, without ripples; the dawn straw white |

Five failed rounds, so the code reverts; the note's `## Outcome` holds the hypothesis: sources without litter (or a prep step rejecting off-hue texels), wind ripples on the sand as their own field driven by `WindState` with the texture's grains on top, and a cap on the brightest straw.

### The miscompile, and the checker that could not see it

Round 3's darkening was not the maths. The textured dunes came out (0.80, 0.74, 0.65) of their untextured selves in linear light, while the texture's means were exactly 1 at every mip and a CPU emulation of the hex blend gave 1.004. Six bisection captures: neutral albedo detail, fine; equal tap weights, fine; `w⁷` alone, fine; the height factor inverted, fine; the original formula with its local `w` renamed `tap_w`, fine; the local kept and the `HexGrid` field `w` renamed instead, fine. A local named `w` beside reads of a struct's field `g.w` renders wrong, silently. The `wesl` output and naga's IR (round-tripped to WGSL) are both correct, and the terrain fragment's own `var w` beside `t.w` renders bit-identically when renamed (a snow scene, three captures, max difference 0), so the fault is downstream of naga's front end or in Bevy's link of an imported module. `AGENTS.md` has the trap.

While dumping the compiled module, `tools/wesl-check` turned out to compile a **library** module to an empty string: stripping keeps only what an entry point reaches, and a library has none, so "the WGSL is ok" checked nothing of a library's functions past the parser — a planted `let bogus: f32 = vec3<f32>(1.0);` in `terrain/common.wesl` passed. With `keep_main` and `visibility: false` (as Bevy compiles the crate) it is named with its line; the crate's 31 checkable modules are clean. Shipped, with the `AGENTS.md` description corrected.

### Sources

- **Andersson (2007)**, *Terrain Rendering in Frostbite Using Procedural Shader Splatting* (corpus `10.1145_1281500.1281668`): colour maps for the low frequencies, tiled detail maps for the high.
- **Heitz & Neyret (2018)**, DOI `10.1145/3233304` (HAL, read): triangle-grid by-example noise, three fetches, variance-preserving blending.
- **Mikkelsen (2022)**, *Practical Real-Time Hex-Tiling*, JCGT 11(3) (read from the URL): the triangle grid, rotation, `γ = 7`, `β = 0.6`, gradients rotated per tile.
- **Akenine-Möller et al. (2018)**, *Real-Time Rendering* 4th ed. (corpus): texture detail, mip-mapping, normal-map filtering.

### Measurements

- Mirror (the attempt, 26 terrain mirror tests pass, three new): the hex weights rebuild the point from the hex centres over 2000 points; a tap's slope turns back by the transpose of its rotation (finite differences, 64 hexes); the height blend keeps the ground's share and is continuous at the gate.
- Means (ground region, sRGB DN, round 5 against the baseline): `hills_12` and `hills_aerial` within 0.3; `scree_down`, `talus_b`, `mtn_foot` within 0.1; the dunes within 0.8; the near meadow views R +2..+6, G −1..−6 (the detail's variance through the tonemap). Without a set (`ELEMENTS_GROUND_TEXTURES=0`): identical.
- **Cost** (`bench_elements`, `hills`, eye level `ELEMENTS_EYE=0,0,0 ELEMENTS_LOOK=12,-3,8 ELEMENTS_AGL=1.7`, three interleaved trials, the last per-pass minimum; clocks P5 for 381 of 451 samples): `main_opaque_pass_3d` base 0.878 / 1.260 / 1.304 ms against the attempt's 0.919 / 2.390 (an outlier) / 1.453 ms: about +0.04 ms on the best pair.
- The packed set: 1024² per layer, 11 levels, zstd: 25 MB (512²: 7 MB, three times magnified underfoot).

### golem, for the user's PBR direction

golem's `generate_image` (FLUX.2 klein 9B, 1024², 4 steps, ~20 s) makes usable top-down ground photographs from a prompt that asks for flat light, no shadows, no vignette and an irregular, isotropic layout; it has no tiling, no PBR maps and no texture tool (`health_check`, `list_image_models`), so the prep tool makes the tiles and derives the maps. Its habits: a radial or grid composition unless the prompt forbids it; sand with ripples (an orientation the hex tiles scatter) unless told "no ripples"; dead leaves added to soil and turf unasked; baked light, ten to one in linear light, which the tool de-lights. A stylistic variant is three albedos and a manifest.

### The next five items, ranked

1. **The ground textures, again**, from `attempt/iter-45-ground-textures`: a soil source without leaves (and the prep tool rejecting texels far from their layer's hue), a cap on the brightest straw, and the dunes kept on their procedural mottle until they have ripples. The meadow is already right.
2. **Wind ripples on the sand**: 5-15 cm ripples oriented by `WindState`, asymmetric (gentle stoss, steep lee), with defects where they bifurcate; research Bagnold 1941 and Anderson 1987 on ripple wavelength and migration.
3. **The rock's PBR texture** on the joint-set structure (`attempt/iter-44-joint-sets`), triplanar in the rock's frame.
4. **Stylistic variants**: a second golem set (painterly) through the same `GroundTextures`.
5. **The world ends in a white plane** at eye level and from the air; the painted scree's dots.

## Iteration 46 - 2026-10-23 - PBR detail textures for the soil and grass

**Commits:** `3a1e83f` (the research note's second attempt), `ea2a24c` (the implementation, the prep tool, the golem set), `bc30992` (README section, showcase `docs/ground_textures.png` and performance, changelog, the note's deviations 17-19 and outcome, two `AGENTS.md` rules), this log. **Sibling repos:** none. **Not released.**

The user's focus for this run: better-looking terrain, variety, rocks and debris, a realistic ground, stylistic variants; PBR textures in everything, especially the terrain; golem (which now offers img2img: `image_path`, `image_strength`; still no texture or PBR-map tool).

### Candidates (`/tmp/i46/findings.md`)

1. **The meadow underfoot is a green felt** (`hills_down`, `hills_eye`, `plains_down`, `plains_seed3`, `hills_dawn`): iteration 45's attempt was right for the meadow and short by three defects.
2. **Rain turns the meadow into a lake** (`hills_rain`, Rain, 3 h grown): every pixel is rippled open water. The hydrology has no infiltration (`hydrology.md`: rain, sources, sinks, evaporation, edge outflow), so 12 mm of rain stays on the surface and its sheet flow crosses the 2 mm puddle threshold everywhere. Class 1.
3. **Snow is a flat white-out** with no surface detail at 1.7 m (`hills_snow`).
4. **The dunes are a cottage-cheese mottle**, no ripples.
5. The cliff's chicken wire; the world's white edge; the scree dots.

The textures beat the flooded meadow: the user's focus names PBR terrain textures, and the attempt was three fixes from GREAT, while infiltration needs its own research (a loss term that must not drain lakes, and the conservation tests). It heads the backlog.

### What changed

From `attempt/iter-45-ground-textures`: `GroundTextures` (albedo detail with mean 1, height, normal, occlusion; hex-tiled; height-blended between layers), with the second attempt's changes:

- **Soil and grass only.** A zero `tile_m` leaves a layer unsampled and procedural, with its whole procedural grain; the examples' set leaves the sand to a future ripple field, so the dunes are bit-identical to the baseline.
- **Sources without litter.** A new golem soil (cracks, clods, grey pebbles, no leaves); for the grass FLUX drew a radial clump and then concentric rings, so the set's grass is an **img2img** pass (`image_strength` 0.6) over the attempt's leaf-free pasture that keeps its layout and greens its straw.
- **The derived roughness is neutral** (the dawn's silver strands were specular from "tops glossier"); **the height blend is band-limited** (1-3 cm footprint).
- **The cost** (below): the textured path compiled only into a set's pipeline (`TerrainExtensionKey`, `GROUND_TEXTURES`), taps and layers accumulated into running sums, negligible taps not fetched, the eight orientations of the square instead of a free angle, the relief fetched under 6 cm of footprint and the albedo detail under 0.5 m, the occlusion a mean-1 multiplier so the fades move no mean.

### Rounds (`/tmp/i46/critique.md`; pairs `/tmp/i46/r{1,3,6}_*.png`)

| Round | Change | Verdict |
|---|---|---|
| 1 | The attempt with the new sources, sand procedural | NOT YET: a 12 m mid-field speckle (high-pass 3.13 against 1.49); silver strands toward the low sun at dawn and dusk |
| 2 | Height blend band-limited | NOT YET: the speckle is the texture's own grain, correctly filtered (a 0.3 m/s glide: it moves coherently, `z_slow.png`); the silver unchanged — and a greener source had not changed it either. Experiment: the roughness detail neutral drops the dawn's p99 from (219, 215, 179) to (199, 201, 152) (`z_dawn_exp.png`) |
| 3 | Derived roughness neutral | GREAT on every angle |
| 4-6 | The cost (below), then the final code re-judged | GREAT: as round 3 on all 20 views, the fades leave no band, nothing pops in the 3 m/s glides (the ground's mean climbs 134.6-135.4 smoothly) |

Per angle, round 6: underfoot (`hills_down`, `plains_down`, `plains_seed3`) real pasture — blades, moss cushions, clover, crumbled red earth with pebbles, no tile repeat, no hex seam; eye level (`hills_eye`, `plains_eye`, `hills_seed7`) the felt gone, blades near, fine strands through the mid field, the palette's patches beyond; dawn green-gold, dusk golden with the turf's relief, `hills_dusk_away` green; overcast, night, rain (the old lake, now with blades under the water), snow (covers as before); 12 m and aerial within 0.1 DN; dunes, scree, talus, mountain foot unchanged. Remaining, cosmetic: the soil's grey pebbles take a faint mauve from the orange `SOIL`; the blades lie flat (standing grass is `bevy_flora`'s).

### Measurements

- **Cost** (`bench_elements`, `hills`, `ELEMENTS_EYE=0,0,0 ELEMENTS_LOOK=12,-3,8 ELEMENTS_AGL=1.7`, 3840 × 2160, base and change interleaved, P5 for most samples; the opaque pass's **mean**, since at 4K the minimum reads 0.000 and at 1080p the clocks hop between P2, P3 and P5): the plan's shader 13.2-14.3 ms against the base's 6.8-7.2 (and 8.9-9.1 with no set). Anisotropy 16/4/1, a 512² set, grass alone, no relief and only the dominant tap all left it within noise: the shader's peak register count was the cost. Final: **no set 6.8-7.4 ms against the base's 7.1-7.3; with the set 8.8-9.4 ms, +26 %**, logged as the trade. The early prepass equal across runs (0.15-0.19 ms).
- Means (ground region, sRGB DN, round 6 against the baseline): `hills_12`, `hills_aerial` within 0.1; rock and dunes 0.0; snow 0.2; the near meadow R +1.5..+5.5, G −0.6..−5.4 (the detail's variance through the tonemap).
- Mirror: 29 terrain mirror tests (new: the fetch cut-off carries at most 0.5 % of the blend over 48 000 samples; the height blend fades where it is not resolved; an untextured layer keeps its grain). Gates 1-5 pass (23 integration GPU blocks, the four library GPU modules, `the_horizon_sky` alone).
- The set: 1024² per layer, 11 levels, zstd: 15.8 MB (`assets/ground/ground.ktx2`, the sand layer neutral).

### Sources

- Iteration 45's (Andersson 2007, Heitz & Neyret 2018, Mikkelsen 2022, RTR4).
- **Baldocchi, Xu & Kiang (2004)**, Agric. For. Meteorol. 123, DOI `10.1016/j.agrformet.2003.11.006` (corpus): the green canopy obscures bare soil and detritus; golden dead grass is summer's.
- **Lagae et al. (2010)**, CGF 29(8), DOI `10.1111/j.1467-8659.2010.01827.x` (corpus): frequency clamping, for the height blend.

### The next five items, ranked

1. **Infiltration, so light rain soaks in** (`hills_rain`: the meadow a lake after 3 h of 4 mm/h). A per-cell loss at the soil's steady infiltration capacity (Horton; saturated conductivity by texture, Rawls et al.), none on rock, and none where a lake's bed is saturated; the puddle threshold perhaps tied to the turf's height. Research first; the conservation budget gains a term.
2. **Wind ripples on the sand**, then the sand's texture: ripples oriented by `WindState`, asymmetric, with defects; the hex-tiled grains on top.
3. **Stylistic variants**: a second set through `GroundTextures` (img2img over this set's sources for a painterly style), and a style switch in the examples.
4. **Snow surface detail** (`hills_snow`: a flat white-out at 1.7 m).
5. **The rock's PBR texture** on the joint-set structure (`attempt/iter-44-joint-sets`); the world's white edge.

## Iteration 47 - 2026-10-23 - Wet ground is no longer a mirror

**Commits:** `3c05386`, `5611bce`, `310c592`, `e293709` (the research note and its three addenda, each before the code it covers), `2c9db2f` (the shading, the canopy, the rain rings, the rock's cavities, the town's ground, tests), `6630e9d` (README, the showcase `docs/ground_cover_rain.png` and `docs/water_flood_rain.png`, changelog, the wetness and precipitation notes' pointers, three `AGENTS.md` traps), this log. **Sibling repos:** none. **Not released.**

The user's focus for this run, unchanged: better-looking, more varied, more realistic ground; rocks and debris; stylistic variants; PBR textures everywhere, especially the terrain, settable on it; golem. Asked mid-run how the PBR textures are going, that they be settable on the terrain, and that open issues come before other enhancements: see items 1-3 below.

### Candidates (`/tmp/i47/findings.md`)

1. **Rain turns every ground into a mirror** (class 1, every rain, drizzle and storm scene). Iteration 46 logged it as "the meadow becomes a lake; no infiltration". Wrong diagnosis: `ELEMENTS_GROW_HOURS` advances the world clock, which wetness integrates, but the hydrology runs in real time, so a 10 s capture holds ~0.01 mm of water. The lake was `weather_surface` lerping roughness to 0.1 at full wetness for every surface: the meadow a choppy grey lake (10-60 m band 187 DN, saturation 0.032, against 124 / 0.308 for the same rain unsoaked), hills at dusk that read as snow, a cliff of chrome foil, dunes white in the middle distance.
2. Rain splash rings on dry grass (unmasked by 1).
3. Infiltration (no visible defect in captures; hours of real-time rain would pond everywhere).
4. Stylistic variants; snow detail; rock texture.

Wet ground beats the runner-up (stylistic variants) because it is a physically wrong result in every rain scene and the user's focus is a realistic ground.

### What changed

- **Porous wet surfaces take Lagarde's limited sheen**, `r (1 − 0.4 w φ)` (0.54 soaked), and the 0.1 film only where a film forms: smooth and metal surfaces, and porous ones drenched past saturation (rain streaks).
- **The wet albedo is the water-film physics per channel**, `(1 − r_e)(1 − r_i) ρ / (1 − r_i ρ)`, the model `wetness.md` derived as the evidence for its flat × 0.5: a dark channel loses more, so wet colour is more saturated (sand darker and browner, not greyer).
- **The grass is a canopy** (its wax beads the rain): `bevy_flora`'s wet leaf (saturation × 1.25, brightness kept, no gloss); only soil, sand, rock, trench and scree darken and shine.
- **The rock's joints and fractures occlude its sky reflection** by the visibility that already darkens them, so a wet cliff keeps its joint network.
- **Rain rings only on standing water** on a live terrain (≥ `WetnessSettings::puddle_min_m`); a blocker's roof and a plane without terrain as before.
- `weathered_town`'s ground is 40 km (its 600 m slab ended in a hard line once the ground stopped mirroring the sky).

### Rounds (`/tmp/i47/critique.md`)

| Round | Change | Verdict |
|---|---|---|
| 1 | Porous gloss, film, canopy | NOT YET: white rain rings littering the grass (storm: dozens); town ground ends in a line; cliff a uniform silver-grey; sand greyer than dry |
| 2 | + rings on water only, town 40 km | NOT YET: cliff and sand. Diagnostics: V1 slope specular occlusion (no gain), V2 no gloss on rock (palette still gone: the unsoaked rain cliff is 0.033 saturation already) |
| 3 | + film albedo per channel (V4), + the rock's cavity occlusion (V3) | GREAT on every view: meadow eye/down/12 m/aerial/dusk, drizzle, storm, plains, cliff (dark joints through a soft sheen), sand (darker, browner), town, puddles, pond rings, flood banks clean. Performance gate failed: +5-10 % on the dry meadow |
| 4 | The canopy share from the albedo's excess green (Woebbecke et al. 1995) instead of the layer weights | GREAT on every view (as round 3; the soil between the blades now darkens as soil); the dry cost back to the base |

A dry-rock difference against the baseline (p99 24 levels) was bisected: wetness 0 there (binary mask), V3 accounts for p99 4, and the rest is the rock's cell network flipping on last-bit differences when the fragment's register schedule changes — a pure no-op that keeps `w.w` and `trench` alive reproduces the canopy line's difference exactly (B2 vs B4: max 1). Each build renders identically run to run. Recorded in `AGENTS.md`.

The performance gate was the harder one. With the opaque pass divided by the same run's early prepass (Xvfb hops between P2 and P5, and the means alone swing 2×; the ratio holds to about ±3 %), removing one piece at a time on the dry meadow: base 55.5-57.4, round 3 59.1-59.4, the canopy's three values folded into one before the rock's material 66.7, without V3 60.5, without the canopy 54.5-55.5 — and without the canopy the meadow greyed again (mid band 144.0/0.068). The canopy's inputs (`w.w`, the trench, the scree band) were dead before `rock_material` on `main` and had to live across it; the albedo is live there anyway. Recorded in `AGENTS.md`.

### Measurements

- Bands (sRGB, rain unsoaked → final): meadow near 115.4/0.347 → 115.7/0.436; 10-60 m 124.0/0.308 → 123.2/0.384 (was 187.3/0.032); look-down 110.8/0.354 → 110.1/0.440; cliff 125.0/0.033 → 127.5/0.031 (high-pass 2.57 → 3.22); sand near 190.7/0.245 → 176.7/0.258, grazing 195.7/0.236 → 187.6/0.199 (Jensen's sheen).
- Dry: `hills_eye`, `hills_overcast`, `hills_snow`, `dunes_overcast` ≤ 1 level on ≤ 0.2 % of pixels.
- **Cost** (`bench_elements`, 3840 × 2160, eye-level meadow and mountain foot, base and change interleaved, the opaque pass over the early prepass; clocks P2/P5/P8, logged in the same shell): dry meadow 55.2-55.6 against 55.5-57.4, **unchanged**; soaked under Rain (`ELEMENTS_GROW_HOURS=3`) the meadow 57.2-58.7 against 54.7-55.3, **+5 %**, the mountain 56.4-57.1 against 55.6-55.8, **+2 %** — the wet path's own arithmetic, logged as the trade for every rain scene's ground.
- Tests: the shading numbers (0.54 / 0.57 / 0.4975, the film table as a gate, saturation 0.667 → 0.736, a film on smooth and metal, the drenched ramp 0.32 at 1.2), the canopy rule and the canopy share (grass > 0.99; sand, rock, soil 0; a grass-soil half 0.34), the GPU wet cube, forward and deferred, at the film albedo and 0.54. Gates 1-5 pass: 23 integration GPU blocks, the library modules (clouds 10, terrain 8, cover 8, dust 6), `the_horizon_sky` alone; the deferred wet test was found by the gate still pinning × 0.5 and 0.1.

### Sources

- Lagarde 2013, *Water drop 3b* (the porous rule, verbatim; the wet/drenched/puddle regimes; "wet surface color is more saturated").
- RTR4 §9.9 (porous darkening is subsurface), the cavity specular trick, Lagarde & de Rousiers 2014 via Bevy's specular occlusion.
- `bevy_flora` 0.46 `wet_on`; Chen et al. 2017 (`10.1038_s41598-017-03360-1`, leaf wax and contact angle); Tatarchuk 2006 (ripples in puddles, splashes on hard surfaces); Xiao et al. 1998 (`10.48044_jauf.1998.028`, canopy interception); Jensen et al. 1999 via `wetness.md` (the film model, forward scattering of wet sand and rock).

### The next five items, ranked

The user asked, during this run, that open issues come before other enhancements.

1. **Issue #35, `TerrainLook`**: a tint and saturation a game sets on the owned terrain (Sanctuary grades its ground per palette; at night the meadow's green shows olive through its navy veil). A uniform on `TerrainMaterialParams` applied to the bare ground's albedo before the cover; identity skipped by a uniform branch; no re-specialisation. The saturation operator is `wet_canopy`'s.
2. **Issue #31, a recent-climate history query**: a rolling record of daily conditions (temperature, humidity, precipitation) a game can summarise ("damp days in the last week"); no game vocabulary in `src/`, so the issue's own alternative (`DayConditions` history), not `GrainCondition`.
3. **PBR textures settable on the terrain** (the user's ask): today `GroundTextures` is a crate-global resource holding one pre-packed KTX2 array (mean-1 albedo detail + height, normal + roughness + AO) that only `tools/ground-textures/prep.py` makes, for sand, soil and grass. A game needs a per-`Terrain` set of ordinary PBR maps per layer (albedo, normal, roughness, AO, height), rock included, packed by the crate, and golem's sets loadable without the Python step.
4. **Puddle shorelines follow the micro-relief**: the puddles are rhombi of the water grid (`cover_rain`), now legible against the matte meadow.
5. **Stylistic variants** (once 3 makes sets settable); the rock's close range (the scree view's giant flat planes); infiltration for long real-time rain.

## Iteration 48 - 2026-10-23 - `TerrainLook`: a game grades the terrain's ground (issue #35)

**Commits:** `f098060`, `13a586c`, `1b12983` (the research note `docs/research/terrain-look.md` and its two amendments, each before the code it covers), `30e38e1` (`TerrainLook`, the shared operator, the scattered rocks, the examples' knobs, tests), `87c3a9c` (README, changelog, the branch-cost trap in `AGENTS.md`), this log. **Issue:** #35, closed with the merge. **Sibling repos:** none. **Not released.**

The user asked during iteration 47 that open issues come before other enhancements; two were open (#35 `TerrainLook`, #31 a recent-climate query). #35 ranked first: it blocks a consumer's night palette today and it is the terrain, the run's focus; #31 is next.

### What changed

- **`TerrainLook { tint, saturation }`** (resource, default identity) grades the owned terrain's bare ground — the albedo after the layers, textures, turf, scree and rock, before the ground cover — with the family's saturation operator (`grey + (c − grey) s` about linear Rec. 709 luminance, clamped at 0), then the tint. The canopy share is read before the grade, so a grey or blue meadow still beads the rain as grass; snow and standing water keep their colours.
- **The scattered rocks follow it**: their shared `WeatheredMaterial` gains a crate-private `look` lane, written from `TerrainLook` when it changes; a game's own `WeatheredMaterial`s keep the identity.
- `TerrainMaterialParams.look` is a uniform lane: a change re-prepares the material and compiles nothing (GPU test, 240 frames).
- `saturated()` is shared with `wet_canopy`; `examples/common::terrain_look_env` gives `terrain_tour` and `bench_elements` `ELEMENTS_TINT`/`ELEMENTS_SATURATION`; `terrain_tour` also `ELEMENTS_GRADE_GLIDE` (a palette blend written every frame).

### Rounds (`/tmp/i48/critique.md`)

| Round | Change | Verdict |
|---|---|---|
| 1 | The grade on the terrain | NOT YET: the scattered rocks stayed warm tan on a cool graded ground (`mtn_graded`) |
| 2 | + the rocks follow the grade | GREAT on every view; the performance gate failed: the identity branch cost +6 % on every frame |
| 3 | Branchless operator | GREAT (branchless vs branched ≤ 9 levels on 0.04 % of the pixels, the rock's flips); cost within noise |

Per view: night (the default night meadow is the issue's olive, graded a dark blue-grey); day and 12 m (a cool, desaturated, coherent grade with every blade and the relief); saturation 0 (monochrome, chromaticity spread 0.0074 dry and 0.0040 wet against the issue's 0.01; the snow frame pixel-identical to the default); tint 0.5 (the CPU mirror within 1e-6); graded rock, sand and scattered rocks (the stones at the cliff's foot the ground's cool family); the glide (61 frames, the ground's L a smooth cosine 130.2 → 116.9, ≤ 1.07 levels a step).

### Measurements

- Default identity against `main`: `hills_eye` ≤ 1 level on 0.05 % of the pixels, `hills_snow` and `dunes_overcast` 0, `mtn_overcast` ≤ 1 level on 5 % (the operator's rounding at the default), `weathered_town` ≤ 1 level.
- **Cost** (`bench_elements`, 4K dry meadow, the opaque pass over the early prepass, base and change interleaved): with the branch 57.3-61.4 against base 54.6-57.5 (+6 %); branchless at the default 56.3-57.7 and 57.5-57.7 against base 57.8, graded 53.9-59.8: no measurable cost. A base run that landed in P2 (prepass 0.075 ms, ratio 63.7) was discarded; the ratio cancels the clocks within a P-state only (`AGENTS.md`).
- Tests: the operator (identity and tint within 1e-6, saturation 0's zero chroma and kept luminance, the order, the clamps), the default packs to (1, 1, 1, 1) and negative lanes to 0, and the GPU test `a_look_written_every_frame_compiles_nothing`. Gates 1-4 pass (24 suites); gate 5: 23 integration binaries ok, `clouds` 10, `terrain` 9, `cover` 8, `dust` 6 and `the_horizon_sky` 1.

### Sources

- Issue #35 (the requirement and acceptance criteria); RTR4 §8.2.3 (colour grading, scene-referred over display-referred; Uncharted 4's multiply); `bevy_flora`'s `wet_on` and this crate's `wet_canopy` (the saturation operator); ITU-R BT.709 luma.

### The next five items, ranked

1. **Issue #31, a recent-climate history query**: a rolling record of daily conditions a game can summarise (the issue's own alternative, `DayConditions` history; no game vocabulary in `src/`).
2. **PBR textures settable on the terrain** (the user's ask): a per-`Terrain` set of ordinary PBR maps per layer (albedo, normal, roughness, AO, height), rock included, packed by the crate; golem's sets loadable without `prep.py`.
3. **Puddle shorelines follow the micro-relief** (the rhombi of the water grid).
4. **Stylistic variants** (once 2 makes sets settable).
5. **The rock's close range**; infiltration for long real-time rain.

## Iteration 49 - 2026-10-23 - A record of recent days (issue #31)

**Commits:** `557ee05`, `b4e738d` (the research note `docs/research/climate-history.md` and its amendment), `e0242cc` (`ClimateHistory`, `DayConditions`, `ClimateSummary`, the `Environment` queries, tests), `75399c7` (README, changelog, the clock-midnight trap in `AGENTS.md`), this log. **Issue:** #31, closed with the merge. **Sibling repos:** none. **Not released.**

The last open issue, taken before any other enhancement as the user asked. No visual change: the deliverable is an API, verified by tests and a headless smoke over every climate preset.

### What changed

- **`ClimateHistory`** (resource, inserted by `ClimatePlugin`, recorded in `ElementsSystems::Weather` after the weather) keeps one `DayConditions` per local calendar day: TG, TN and TX (the mean, minimum and maximum temperature at the reference elevation), the mean relative humidity, RR (the precipitation total, liquid-water equivalent) and the snowfall, and the hours observed. Each frame adds the game time since the last with that frame's `Weather`, split at midnight, summed in f64. A step of up to a day is recorded (the point snowpack's 24 h), a longer jump leaves the days between unrecorded, a rewind forgets the days from its new date on, and a load more than an hour off the saved clock re-arms. 31 days by default (`with_capacity`).
- **The indices**: `WET_DAY_MM` (1 mm) and `FREEZING_C` (0 °C) with `DayConditions::is_wet`, `is_frost` (TN < 0) and `is_icing` (TX < 0), the ETCCDI's definitions. `ClimateHistory::recent(n)` / `summary(n)` and `Environment::recent_days(n)` / `recent_climate(n)`: the completed days of the last `n` calendar days and their `ClimateSummary` (counts, hour-weighted means, extremes, totals); 0 is empty, a longer window is the record, no days is `None`.
- `Environment` takes the record as `Option<Res<_>>`: a world built without `ClimatePlugin` keeps working, with no days.
- With `serde` the record joins the weather's save set.

### Rounds

| Round | Finding | Change |
|---|---|---|
| 1 | The frame-step test failed: at 1.5-minute frames two days were dated 10 January. At 2.3×10⁵ hours, `days × 24 + 23.999999999999996` rounds to the next midnight, so the recorder saw the day change while `clock.date` still said the old day, and every later day shifted by one. | The day's date is derived from the hour count itself (`date_of`). `AGENTS.md` trap. |
| 2 | All tests pass; the smoke over the presets shows the weather's precipitation totals far above climatology (below). | Recorded as a finding (`climate-history.md` Deviation 4); the next item. |

### Measurements

- Determinism: two runs of temperate seed 42 for 10 days give identical records and summaries.
- Frame step: at 6- and 1.5-minute frames a temperate January week differs by at most 0.004 °C in the daily means, 0.014 °C in the minima, 0 in the maxima, 0.0007 in the humidity and 0 in the totals; the day counts are identical (the test's bounds are 3–5× that).
- Save: restored onto the saved clock, the record matches the original exactly over 72 more game hours.
- **Smoke, 30 days through `Environment::recent_climate(30)`, seed 7**:

| Climate, month | Wet days | Frost days | Icing days | Mean °C | Min °C | Max °C | RH | Precipitation mm |
|---|---|---|---|---|---|---|---|---|
| Temperate, January | 12 | 30 | 0 | 1.8 | −7.0 | 9.2 | 0.73 | 231 |
| Temperate, July | 16 | 0 | 0 | 19.0 | 9.9 | 27.0 | 0.77 | 1342 |
| Desert, July | 3 | 0 | 0 | 33.7 | 15.9 | 50.0 | 0.58 | 46 |
| Tundra, January | 11 | 30 | 30 | −26.5 | −36.0 | −19.6 | 0.70 | 429 (all snow) |
| Tropical, July | 26 | 0 | 0 | 25.8 | 17.9 | 37.0 | 0.92 | 7844 |
| Alpine, January | 12 | 30 | 17 | −8.9 | −21.0 | 1.2 | 0.72 | 533 (all snow) |

The temperatures and day counts are plausible. **The amounts are not**: Richardson's observed Atlanta (1204 mm a year, a wet station) has 59–125 mm and 5.5–10.7 wet days per 28 days; the temperate preset rains 2–13× that. A defect of the scheduler (the wet presets' share and rates), not the record.

- Cost: one CPU system of constant arithmetic per frame; no GPU work; not benchmarked.
- Tests: six unit tests (a steady day, the midnight split, the jump and the rewind, the window and the capacity, the ETCCDI thresholds, the hour weighting), two integration tests (`tests/climate_history.rs`) and one save test. Gates 1-4 pass (25 suites; `tests/save.rs` with `--features serde`, 6); gate 5: 24 integration binaries ok, `clouds` 10, `terrain` 9, `cover` 8, `dust` 6 and `the_horizon_sky` 1.

### Sources

- Issue #31; the ETCCDI's definitions of the 27 core indices (FD, ID, the 1 mm wet day); Zhang et al. 2011 (WIREs Climate Change, the indices; not in the corpus, paywalled); Thurner et al. 2017 (frost and icing days); Richardson 1981 (the daily weather record; the observed Atlanta table).

### The next five items, ranked

1. **The weather's precipitation climatology**: the temperate preset rains 2–13× an observed wet station's totals on ~1.5× its wet days (this iteration's smoke). Calibrate the wet presets' share and rates against observed totals per climate, so the record, the snowpack, the hydrology's rain and the cover maps get realistic amounts. It changes every domain's weather and needs its own research and captures.
2. **PBR textures settable on the terrain** (the user's ask): a per-`Terrain` set of ordinary PBR maps per layer (albedo, normal, roughness, AO, height), rock included, packed by the crate; golem's sets loadable without `prep.py`.
3. **Puddle shorelines follow the micro-relief** (the rhombi of the water grid).
4. **Stylistic variants** (once 2 makes sets settable).
5. **The rock's close range**; infiltration for long real-time rain.

## Iteration 50 - 2026-10-23 - PBR textures set on the terrain

**Commits:** `adc9a7f`, `20ecaa2`, `71587f4` (the research note `docs/research/terrain-textures.md` and two amendments, each committed alone before the code it covers), `737cde2` and `c807df0` (`TerrainTextures`, `GroundLayerTextures`, the in-crate packer, the `GROUND_COLOURS` pipeline, the examples' cutover, `GroundTextures`/KTX2/`prep.py` removed; a clippy fix), `114a569` (README, changelog, `AGENTS.md`), this log. **Sibling repos:** none. **Not released.** `docs/ground_textures.png` is unchanged: the golem set renders identically through the new path.

### Why this item

No issue was open. The user asked twice for the ground's PBR textures to be settable on the terrain, with golem's help, ahead of the backlog's first item (the weather's precipitation climatology, a defect found in iteration 49), which is the runner-up: it is physically wrong but invisible in a frame, where a game today cannot hand the terrain its own maps at all. Before this iteration the textures were one crate-wide resource holding a KTX2 array that only an offline Python tool could make, and that tool threw a PBR set's normal, roughness, occlusion and height maps away. golem's tools are unchanged in this session (FLUX image generation and img2img; no texture or PBR-map tool, no texture-specific model), so its sets remain albedos whose maps the crate derives.

### What changed

- **`TerrainTextures`** (component on the `Terrain` entity): per ground layer (sand, soil, grass) an `Option<GroundLayerTextures>` — a base colour and, optionally, a normal (+Y up; `flip_normal_map_y` for DirectX), roughness (G), occlusion (R) and height (R) map, read as glTF stores them whatever sRGB flag the images carry; one ARM/ORM image serves as both roughness and occlusion. `tile_m`, `normal_scale`, `relief_deg` (derived normals), `contrast` (de-lighting), `tileable`, `colour`. `GroundLayerTextures::photograph()` de-lights and tiles a photograph or a generated image; `with_colour(share)` takes the set's own mean colour.
- **The packer is in the crate** (`src/terrain/textures.rs`): an async task on `AsyncComputeTaskPool` reads the maps (a direct path for 8-bit RGBA, `get_color_at` otherwise), resamples them to `resolution` (1024), and packs the six-layer detail array with every mip level the shader already samples: `prep.py`'s derivations ported, its Gaussian blurs as Kovesi's four-box cascade (running sums, no transposes). Given roughness and occlusion become multipliers with mean 1; a given height is band-passed like a derived one. Inserting, changing or removing the component re-textures the ground at runtime; the previous set renders until the new one is packed.
- **The set's colour is opt-in and has its own pipeline**: `colour` defaults to 0 (the crate's calibrated colour, the set its detail); a share above 0 sets `TerrainExtensionKey::ground_colours`, which compiles the colour lanes (`GROUND_COLOURS`) into that set's pipeline only.
- `GroundTextures`, `GroundTextures::load`, `assets/ground/ground.ktx2` (16 MB) and `tools/ground-textures/` are removed; the golem sources and their prompts moved to `assets/ground/golem/`. The examples hand them to `TerrainTextures` as photographs (`ELEMENTS_GROUND_TEXTURES=0` off, `ELEMENTS_GROUND_SWAP=S` toggles at runtime, `ELEMENTS_GROUND_COLOUR=c` sets the share); the examples' Bevy enables `jpeg`.

### Rounds (`/tmp/i50/critique.md`)

| Round | State | Finding | Verdict |
|---|---|---|---|
| 1 | The plan: PBR sets take their own colour (`colour` 1); lanes read in the textured pipeline | Golem set identical to main; a CC0 set (ambientCG `Grass004` + `Ground037`) bleached the hills to lime and beige at every distance: library base colours 2.1× and 6.5× the crate's | the CC0 look FAIL; the rendering faithful |
| 2 | `colour` 0 by default; ambientCG and then Poly Haven (`sparse_grass` + `farm_soil`, ARM) captured at 16 angles | Correct (calibrated colour, relief lit from the sun's side, no ring, no seam, seamless swap) but both read as a mown lawn with a red speckle where the soil shows; the colour lanes cost +7.7 % at share 0 | CC0 sets NOT YET as a look; performance gate failed |
| 3 | Lanes behind `GROUND_COLOURS`; the examples keep the golem set only | Default identical to main, its cost within noise; the opt-in colour pipeline works at +8.6 % | GREAT |

### Measurements

- **Packer = prep.py**: the golem albedos packed by the crate against the KTX2, level 0: mean |d| ≤ 1.16 levels of 255, p99 ≤ 5, every channel mean identical, the neutral sand bit-identical.
- **Golem set through the new path vs main**, 16 angles (`hills` eye/down/12 m/aerial/dawn/dusk both ways/overcast/rain/snow/seed 7, `plains`, `dunes`, scree, mountain foot): mean |d| ≤ 0.49 DN, p99 ≤ 2 DN, frame means within 0.02 DN; no set: ≤ 1 level on < 0.05 % of pixels.
- **The normal convention on real maps**: the packed `NormalGL` normals correlate with the normals derived from the same sets' displacement maps at +0.50 (soil) and +0.40 (grass) in u and v; with `flip_normal_map_y` the v correlation turns to −0.50 and −0.40.
- **Runtime swap** (2 s cycle, 251 frames): golem → CC0 handed over at frames 136 and 494, 14-16 frames after the swap, with no procedural frame in between.
- **Packing time** (desktop CPU, one background task): two layers of five 1024² maps 0.48 s, the golem photographs 0.62 s; the first version took 1.18-1.39 s (40 ms map reads through `get_color_at`, 88 ms blurs with transposes) — now 10 ms and 20 ms.
- **Cost** (`bench_elements`, 4K eye-level meadow, opaque pass over the early prepass, interleaved with `main`, P5): the golem set 55.7-59.7 against 55.7-57.8 (raw pass 9.24-9.35 ms vs 9.25-9.52), no set 45.6-46.5 vs 45.0-45.2 — both unchanged; the lanes read in the textured pipeline (round 2) 59.8-63.5 against 55.6-60.3, +7.7 %; a set taking its colour 61.0-61.7 against 56.0-57.1, +8.6 %, opt-in.
- Tests: the box cascade's σ and sum, every mip level's details averaging to 1, a flat colour packing no detail and its own colour, OpenGL and DirectX normal maps agreeing with the height's slope, given roughness and occlusion as mean-1 details (ORM), a large map packed at the resolution, and a set on the terrain packed, swapped with the old one bound, and removed (headless). Gates 1-4 pass (25 suites; clippy after one fix, `as_chunks`); gate 5: 24 integration binaries ok, `clouds` 10, `terrain` 9, `cover` 8, `dust` 6 and `the_horizon_sky` 1.

### Sources

- Unity, *Terrain Layers* (per-layer diffuse, normal map with normal scale, mask map, tile size); Khronos, *glTF 2.0* §3.9 (sRGB base colour, roughness G, occlusion R, normals +Y up, normal scale); Bevy 0.20 (`flip_normal_map_y`, `Image::get_color_at`); Kovesi 2010, *Fast Almost-Gaussian Filtering* (DICTA, author's copy); Wells 1986 (TPAMI; pending, paywalled); `docs/research/ground-textures.md` (Andersson 2007, Mikkelsen 2022, Heitz & Neyret 2018, the derivations); ambientCG and Poly Haven CC0 sets (trials, not shipped).

### The next five items, ranked

1. **Stylistic variants of the ground** (the user's ask, now that a set is settable and swappable): golem-generated sets in other styles (painterly, cel-shaded, autumn, arid), each judged GREAT and taking its own colour through `with_colour`, with a capture tour; and a calibrated-albedo check for a set's colour (the CC0 finding: library base colours run 2-6.5× the crate's).
2. **The weather's precipitation climatology** (iteration 49's finding): the temperate preset rains 2-13× an observed wet station's totals.
3. **Textures on the rock** (the user: PBR into everything, especially terrain): a triplanar PBR detail on the cliffs over the procedural rock material, settable like the ground's.
4. **A long-bladed meadow from a scanned set**: CC0 scanned grasses are short swards; a set's detail over the crate's rust-brown soil reads as red flecks where the soil shows — a soil-layer contrast limit or a colour share for the soil alone.
5. **Puddle shorelines follow the micro-relief**; the sand's ripple field.

## Iteration 51 - 2026-10-25 - The terrain under an orthographic camera is drawn at full detail on its own samples (issue #36)

**Commits:** `4a28dad`, `11f0e2d` (the research note `docs/research/orthographic-lod.md` and its amendment, each committed alone before the code), `d7f3bd8` (the LOD centre, the uniform lanes, the vertices on the samples, the rocks' seat, tests, the capture path in the examples), `2c8f55b` (README, changelog, `AGENTS.md`, `terrain.md` Deviation 21, `docs/orthographic_lod.png`), this log. **Issue:** #36, closed with the merge. **Sibling repos:** none. **Not released.**

### Why this item

Issue #36 was the one open issue, and the user asked (iteration 47) that open issues come before other enhancements. It is also a visible defect in a consumer's game: Sanctuary's figures sink into the ground its isometric camera draws ("Is it meant to be mud?"). The runner-up, the backlog's stylistic ground variants, is a beauty gap with no consumer blocked on it.

### What changed

- **The LOD centre.** Under an orthographic followed camera the CDLOD patches are selected and morphed about its `ElementsFocus`; without one, about where its forward ray first meets the ground (marched over the height readback every half cell between the terrain's lowest and highest surface, 16 bisections; `TerrainSnapshot::ray_hit`); a view that meets no ground keeps the eye. A perspective camera selects and morphs about its eye, as before. `TerrainRuntime::lod_centre` is found in `ElementsSystems::Simulation`, so `update_patches` (PostUpdate) and the materials agree in the frame.
- **One centre in every view.** `TerrainMaterialParams::lod_centre` and `WaterParams::lod_centre` (xyz the centre, w 1) replace `view.lod_view_world_position` in the vertex stage (`morph_centre` in `terrain/common.wesl`), so the main pass, the depth prepass and the shadow views displace the same vertices. A walking focus rewrites the uniform every frame, as the rain's ripple phases already do.
- **The vertices on the height samples.** A patch's grid started at the grid's corner, half a cell off the cell-centred samples, so a level-0 vertex was the mean of four samples and even level 0 drew a smoothed map: p90 0.15 m off the bilinear on the hills at 4 m cells, 0.62 m on the mountains. `patch_transform` now places patches half a cell in, the vertex stage clamps a vertex past the last sample onto it (`last_sample`), and the node boxes move with the vertices: level 0 draws the samples themselves, the bilinear at every vertex and within a quarter of a quad's twist between them (p90 0.047 m hills, 0.20 m mountains). The drawn terrain spans the cell centres, half a cell inside the extent.
- **The scattered rocks** seat on the shifted lattice and morph their seat about the same centre.
- **The capture path** (examples): `terrain_tour` and `bench_elements` take `ELEMENTS_ORTHO=<view m>` (an isometric orthographic camera 457 m back from a focus on the ground, `ELEMENTS_PITCH`, `ELEMENTS_HEADING`, `ELEMENTS_WALK`) and `ELEMENTS_PINS=1` (posts on `terrain_height` every 2 m with 10 cm red feet, kept on the query every frame); `bench_elements`' scene line ends with the frame's mean time.

### Rounds (`/tmp/i51/critique.md`)

| Round | State | Finding | Verdict |
|---|---|---|---|
| 1 | Focus centre, uniform lanes, vertices on the samples, rocks | Hidden feet: hills 24 → 0 %, second site 16 → 3 %, seed 7 21 → 1 %, 29 m view 26 → 1 %, 9 m 5 → 0 %, dunes 14 → 0 %, plains 0 → 0 %, mountains 45 → 22 % (the rugged rock's in-quad residual: CPU estimate 14 % of the posts > 8 cm under, 19 % on slopes > 45°); walks keep the feet on; 400 m views and 20 m/s walks: no crack, hills motion-compensated frame change p99 1.6 vs 1.5; perspective set unchanged but for silhouettes | GREAT on every angle (mountains: GREAT for the contract, the residual logged) |

Evidence: `docs/orthographic_lod.png` (before: most posts end in grass; after: every post on its red foot), `/tmp/i51/pair_hills2.png`, `pair_dawn.png`, `cmp_mtn.png`, `diff_hills_12.png`.

### Measurements

- **The drawn level-`k` surface minus the bilinear**, 200 000 points, `High` (512 cells of 4 m): see `orthographic-lod.md` Parameters (level 0 on corners p90 0.151 / on samples 0.047 m on the hills; level 1 0.31, level 2 0.85).
- **Feet** (`feet.py`, posts kept on `terrain_height`): the table in Rounds; every walk frame sampled.
- **Temporal** (`comp.py`, frame change blurred 15 px, global shift removed): hills 400 m walk at 20 m/s, before p99 1.5 / max 1.7, after 1.6 / 1.6; underlay red in no frame of any walk or wide view. The mountains' 400 m walk has local spikes (22 before, 40 after) only where the example's far plane (914 m) cuts the massif, absent with a still camera.
- **Cost** (`bench_elements`, 4K, interleaved with `main`; the GPU shared, so every kept run sat in P2 at 1740-1935 MHz, logged per run): eye-level meadow (perspective), opaque 2.510 / 2.602 ms against 2.560 / 2.561 (opaque over early prepass 69.7 / 74.3 against 73.1 / 73.2), shadow 0.137 / 0.096 against 0.095 / 0.094 — unchanged; the isometric view (`ELEMENTS_ORTHO=18`), opaque 3.070 / 2.898 against 2.781 / 3.123, prepass 0.012 / 0.011 against 0.012 / 0.013, shadow 0.029 / 0.027 against 0.033 / 0.032 (+4 µs: the shadow view now draws the level-0 patches about the focus). Runs in mismatched P-states (P5/P8 against P2) discarded. The ray march (orthographic camera without a focus) costs 2 µs a frame on the CPU (1000 rays over the mountains); with a focus there is none. The frame mean under Xvfb at 4K is presentation-bound (228-684 ms) and says nothing.
- Tests: `an_orthographic_focus_is_drawn_at_level_0_on_the_samples`, `walking_the_focus_neither_pops_nor_cracks` (12 walks of 100 m), the crack and no-pop tests on the shifted lattice (helpers shared), `a_view_ray_meets_the_ground_where_it_first_crosses_it`, the rocks' seat on a non-separable surface, and the GPU test `an_orthographic_camera_selects_and_draws_about_its_focus` (centre = focus, level 0 under it, every material's lane, no clear-colour pixel in a 400 m view). Gates 1-4 pass (clippy after one fix, `as_chunks`); gate 5: the integration binaries ok, `clouds` 10, `terrain` 10 (the new GPU test), `cover` 8, `dust` 6 and `the_horizon_sky` 1.
- **The gates on the merged tree** first hung: a doctest's link slept 2.5 h in `cargo test` and the rest failed with "No space left on device". `/tmp` was full: the timed-out bench runs had written 3.7 GB of `CommandQueue` warnings (iteration 50's left one of 2.8 GB). The logs reduced to their result lines, gates 1-4 pass on the merged tree (25 suites). `AGENTS.md` trap.

### Sources

- Issue #36; Strugar 2009 (CDLOD; one vertex per texel, objects floating or intersecting under unpredictable LOD, the shadow map's LOD from the main camera); Asirvatham & Hoppe 2005 (clipmaps centred on the viewer); Luebke et al. 2003 Eq. 3.1 and Lindstrom et al. 1996 (screen-space error; orthographic: independent of distance); RTR4 §4.7.1; Epic's Landscape Technical Guide and Unity's terrain settings (a vertex on every height sample); Bevy 0.20 `bevy_render/src/view/mod.rs:1244-1276`.

### The next five items, ranked

1. **Shadow bands on the rock under an orthographic camera** (pre-existing, both trees): dark bands 5-12 levels deep across the mountain rock in the isometric view, diagonal and stronger at 9 h (`/tmp/i51/bands_h9.png`), absent at heading 135°: the single orthographic cascade against the rock's relief, to be found and fixed.
2. **Scattered rocks under an orthographic camera**: none are drawn (their `VisibilityRange` and shrink count from the eye 457 m back); range and fade about the focus.
3. **The weather's precipitation climatology** (iteration 49): the temperate preset rains 2-13× an observed wet station's totals.
4. **Stylistic variants of the ground** (the user's ask): golem sets in other styles taking their own colour, with a capture tour.
5. **A drawn-surface query** for figures on rugged ground (the level-0 triangles, not the bilinear: the mountains' residual); textures on the rock.

## Iteration 52 - 2026-10-26 - Paths and roads: researched, NOT YET, not shipped

**Commits:** `c46540e` (the research note `docs/research/paths.md`, committed alone before the code), this log. **The implementation was reverted**: `src/lib.rs`, `src/plugin.rs`, `src/terrain/{material,mod}.rs`, `src/terrain/render.wesl`, `examples/trails.rs`, `tools/wesl-check/src/main.rs` are back at `main`, and `src/paths.rs`/`src/paths.wesl` are deleted. **Issue:** none open. **Sibling repos:** none. **Not released.**

### Why this item (and the runner-up)

The user's instruction for this run: "Can we please focus on roads, path, trails, etc." The crate presses what a *point* does to the ground (`Imprint`, the trails field) and has **nothing** for the ground a *route* wears: the baseline captures (`/tmp/i52/before/snow_eye.png`, `mud_close.png`, `sand_noon.png`) show the terrain's meadow, snow and sand with only the walker's and the vehicle's own marks, and no lane, path or road anywhere. The runner-up was the backlog's first item (the orthographic camera's shadow bands, iteration 51), which is a smaller, narrower defect than a whole missing domain.

### What was built, and what it did

The full design is in `docs/research/paths.md` (`Model adopted`, `Parameters`, `GPU mapping`, `Validation targets`, `What great looks like`). Built and working:

- `Path` (`points`, `half_width_m`, `PathKind::{Earth, Gravel, Stone, Snow}`, `rut_depth_m`), `PathNetwork` (resource), `PathsPlugin` (in `ElementsPlugins`), `PathState`.
- A CPU rasteriser into a **coverage distance field** (`rgba16float`: signed edge distance, kind + packed rut depth, lateral offset as a fraction of the half width, edge fade), off the frame on `AsyncComputeTaskPool`; six unit tests (the signed field, the coverage's edge, a diagonal route's isotropic edge, crossing routes and determinism, clipping, the half conversion) — all passing.
- The mask resolution follows the **narrowest** route (four texels across it), and that was the iteration's central discovery: at a metre a texel a 70 cm footpath is a fraction of one sample and simply cannot be drawn (`/tmp/i52/smoke_mud.png` showed a texel staircase; at 0.175 m texels it was smooth, `smoke_lane_close.png`).
- The terrain material's `path` lane, a `PATHS` pipeline key and shader def, the `bevy_elements::paths` WESL library (`path_at`, `path_colour`, `path_grain`, ruts and a cart track's grassy crown), and a block in `render.wesl` that mixes the worn surface into the albedo, relief and roughness.
- The example laid three routes (`ELEMENTS_PATHS=lane|track|road|all`).

It **rendered**: `/tmp/i52/smoke_lane_close.png` shows a trodden brown lane winding up the hill with a soft ragged edge, and `track_flat4.png` shows a cart track's two rut lines running to the horizon. But the final rounds' capture (`/tmp/i52/diag3.png`) shows the track as **two pale-blue scratches on grass with no bare earth**, while the CPU mirror of the same shader reports `BARE 1.000` across the whole route (`paths::probe_track_cover`). `specialize` logs `paths true` and pushes `PATHS`, and `update_materials` logs the mask reaching the material (`4096 texels at 0.5 m`), so the disagreement is in what the fragment reads, not in the wiring.

### Rounds (`/tmp/i52/critique.md` was not written: the feature never reached a verdict)

| Round | Change | Finding |
|---|---|---|
| 1 | Mask at a metre a texel, tint = soil × colour | The lane was a **transparent smear**: a 0.35 m lane is a fraction of a 1 m texel (`smoke_paths.png`). |
| 2 | The lane's fade made width-relative | Still a smear: a metre's texel cannot resolve a footpath at all. |
| 3 | Mask resolution from the narrowest route | **The lane rendered** (`smoke_lane_close.png`): brown, soft-edged, winding. |
| 4 | Absolute path colours (a dry-earth brown, pale gravel) | The gravel read as **cobblestone** (chips too coarse and too contrasty); fixed to 2 cm chips. |
| 5 | A cart track's grassy crown between its ruts | The track became two **thin pale scratches** (`track_flat4.png`): the bare surface between the wheels vanished with the crown. |
| 6 | The wheel bands bared regardless of the crown | No change: the bare colour still did not appear. |
| 7 | The path block moved after the canopy | No change. |
| 8-10 | Instrumentation: the mask reaches the material, `specialize` gets `paths true` | The fragment's `path.cover` or its `uv` is wrong while the CPU mirror says `BARE 1.0` — **not found in the rounds spent**. |

**Verdict: NOT YET.** The feature is reverted rather than shipped, per the loop's rule after five failed rounds on one feature: "log the blocker, the remaining flaws and your best hypothesis, and revert to the last GREAT state rather than shipping something weaker."

### The blocker and the next attempt's hypotheses

The terrain draws the path **rut** (which comes from the pressed trail field) but not the path's **bare surface** (which comes from the mask sample in the fragment), although the same formula gives 1.0 on the CPU. Hypotheses, in the order to test them:

1. **The sampled `uv` is outside `[0, 1]`** and `path_at` returns `path_none()` before the coverage is computed: print `material.path` and `uv` from the shader once (`debug_printf` or a debug colour output) rather than reasoning on the CPU. The lane is `(origin, texel_m, texels)`, 4096 texels at 0.5 m over the terrain's 2048 m extent; the fragment's `pv.xz` is *displaced and morphed* — if `pv` is not the metre frame the mask was built in, every `uv` falls off the mask. The trail field's own `trail_fade` uses the same convention and does draw, which points at the `uv`'s **origin** difference rather than its scale.
2. `path.colour` mixes *before* `weather_surface` and is overwritten: ruled out by reading `weather_surface` (it takes `base_color` and returns it), but a capture with `snow = 0, wet = 0` would settle it in one frame.
3. The `PATHS` def is pushed but the *bound* pipeline is another variant (the ground material's, or the prepass): the block is in the forward fragment only, and the terrain uses one shader for all its pipelines (`render.wesl`), so a prepass-only path would show exactly this.

A cheaper and more robust alternative, if (1) is confirmed: **bake the worn surface into the terrain's layer colours on the CPU** — a per-cell `worn` lane in the terrain's own readback/`terrain_cells` buffer, written by the rasteriser at the simulation grid's resolution (4 m at High, too coarse for an edge, but enough for "this hillside is trodden") — and keep the *shape* (edge and ruts) in the masks. That trades the sub-texel edge for a pipeline the crate already proves works, and would want its own research pass.

### Measurements

- The mask rasterises 4096² in about 40 ms on the async pool (one thread, measured in the example's first frames); the coverage tests all pass.
- **No performance gate was run** on the reverted code: it does not exist in the tree.
- Baseline captures (`/tmp/i52/before/`, 9 shots: snow at 10.5 h and 15.6 h, sand at noon and 17.5 h, mud, a game's own mesh, and an aerial) confirm the domain is absent, not broken.

### Sources

- Pouyat et al. 2020 (corpus `10.1007_978-3-030-45216-2`): the trail-impact paragraph (bared soil, compaction, drying, vegetation loss, traffic concentrating); Andersson 2007 (corpus `10.1145_1281500.1281668`): mask-based terrain compositing and the 4 px/m destruction mask; Green 2007 via RTR4 §15.4.3: coverage distance fields; Sumner et al. 1999; Tatarchuk 2006; Barré-Brisebois 2014. The full list and what each contributes is in `docs/research/paths.md`.

### The next five items, ranked

1. **Finish paths and roads** with the hypotheses above; the research note, the rasteriser and its six tests are already committed, so the next pass starts at the shader sample.
2. **Shadow bands on the rock under an orthographic camera** (iteration 51's first item, unchanged).
3. **Scattered rocks under an orthographic camera**: none are drawn.
4. **The weather's precipitation climatology** (iteration 49).
5. **Stylistic variants of the ground** (the user's ask).

## Iteration 53 - 2026-10-26 - Paths and roads: narrowed to one fault, NOT YET, not shipped

**Commits:** the research note is iteration 52's (`c46540e`); this log. **The implementation is reverted again**: `git status --short` is empty and `main` builds. **Issue:** none open. **Sibling repos:** none.

### Why this item (and the runner-up)

The user's focus for this run, twice now: "Can we please focus on roads, path, trails, etc." The crate presses what a *point* does to the ground (`Imprint`, the trails field) and has no route surface at all. The runner-up was iteration 51's orthographic shadow bands. Taken again because iteration 52's blocker was one unknown link, not a design problem.

### What this iteration established (the value it leaves behind)

Iteration 52 ended with "the fragment's `path.cover` or its `uv` is wrong while the CPU mirror says BARE 1.0 — not found in the rounds spent". This iteration **found which links are sound and which is not**, by instrumenting rather than re-deriving. Every one of these is now verified, not inferred:

| Link | Verdict | Evidence |
|---|---|---|
| The mask is rasterised | **sound** | `paths::probe_track_cover` on the CPU: `BARE 1.000` across the route; 10 unit tests pass (signed distance, coverage edge, isotropic diagonal edge, wheel-line lateral offsets, crossing routes, clipping, resolution, fade, halves) |
| The mask reaches the material | **sound** | `DIAG materials: 1 / has mask true lane Vec4(-1024.0, -1024.0, 0.5, 4096.0)` — the corner, the texel edge and the texels a side are all correct |
| `PATHS` reaches the forward pipeline | **sound** | `specialize: paths, fragment true label Some("pbr_opaque_mesh_pipeline")` (and a second variant once `GROUND_TEXTURES` arrived) — the prepass gets `fragment false`, which is Bevy's own fragment and irrelevant |
| The `PATHS`/`PATH_DEBUG` block compiles and is linked | **sound** | a planted type error inside the block produced two `parsing error` lines; removing it silenced them |
| The block **runs** | **sound** | the debug paint replaces the frame (green, then cyan) — the block executes every fragment |
| The uniform's lane in the fragment | **sound** | the debug shows lane-ok (green), i.e. `material.path.w > 0.5` |
| The mask **sample** | **BROKEN** | the debug readout is `0.5 / 0.5` everywhere: `textureSampleLevel(path_mask, ...)` returns a distance of exactly 0 at the screen's own `uv` **and** at `vec2(0.5, 0.5)` |

So the one remaining fault is that **the mask texture reads zero on the GPU**, whatever its contents. Tried and ruled out, each with a capture:

- `Rgba16Float` and `Rgba32Float` (same zero).
- `RenderAssetUsages::default()` and `RENDER_WORLD` (`/tmp/i53/dbg12.png`, `dbg13.png`, `dbg15.png`).
- `TextureUsages::TEXTURE_BINDING | COPY_DST` (the trail field's own usage).
- A mask forced to a constant "inside a route, −1 m" on the CPU (`ELEMENTS_PATHS_SELFTEST`, `/tmp/i53/dbg8.png`): still zero, so it is not the field's contents.
- A coarse and a fine mask (the rasteriser's resolution rule works: 0.175 m texels for a footpath).
- No GPU limit or validation error is logged; the sampler is linear, `ClampToEdge`.

What was **not** ruled out, in the order to try next:

1. **The bind group holds a fallback.** The `Option<Handle<Image>>` at bindings 109/110 may be resolved to Bevy's fallback image when the material's *first* bind group is built, and the later asset modification may not re-create it. The crate's ground textures use the same pattern and *do* work (iteration 50), which argues against it — but they also arrive with a full pipeline re-key. **Test:** bind a synthetic 4×4 mask **before** the terrain is spawned (insert `PathState` in `Startup` with a constant image) and see whether it reads.
2. **A 4096² upload is silently dropped.** 4096² `rgba16float` is 128 MB (256 MB at `rgba32float`); both read zero. **Test:** a 64² mask over a small terrain (`Terrain::flat(4.0).with_extent(Vec2::splat(128.0))`), which is a 2-minute capture.
3. **A `texture_2d<f32>` bound in a *combined* material bind group at those indices clashes** with another binding the extension also declares. The extension now declares 100-110; **test:** move the mask to the free 111/112 and re-capture.

### Rounds

| Round | Finding |
|---|---|
| 1-2 | The uniform, the mask's existence and the specialize key verified by logging. |
| 3 | A planted shader error proved the `PATHS` block links. |
| 4-6 | The debug paint proved the block *runs*, and that the lane is set. |
| 7-9 | The readout proved the sample is 0.0 at the screen's `uv` **and** at the mask's centre, in `Rgba16Float`, `Rgba32Float`, `RENDER_WORLD`, and with a constant field. |
| 10 | Stopped per the loop's rule (5 rounds), reverted. |

### Measurements

- The rasteriser: 4096² in ~40 ms on the async pool; 10 unit tests.
- No performance gate (the code is not in the tree). The baseline is unchanged: `main` builds, 313 lib tests pass.

### Sources

As iteration 52 (`docs/research/paths.md`): Pouyat et al. 2020 (bared soil and compaction), Andersson 2007 (mask-based terrain compositing), Green 2007 via RTR4 §15.4.3 (coverage distance fields), Sumner et al. 1999, Tatarchuk 2006.

### The next five items, ranked

1. **Paths and roads**, now starting at the three tests above (a synthetic mask bound before the terrain; a 64² mask; the binding moved to 111/112). The design, the rasteriser, its tests and the diagnostic method are all settled.
2. **Shadow bands on the rock under an orthographic camera** (iteration 51).
3. **Scattered rocks under an orthographic camera**: none are drawn.
4. **The weather's precipitation climatology** (iteration 49).
5. **Stylistic variants of the ground** (the user's ask).

## Iteration 54 - 2026-10-26 - Paths and roads: made to render, NOT YET at the visual gate, reverted

**Commits:** the research note's `Deviations` (this iteration), the three traps in `AGENTS.md`, this log. **The implementation is reverted again**: `git status --short` is empty, `main` builds. **Working sources kept at `/tmp/i54/keep/{paths.rs,paths.wesl,trails.rs}`, and the patches are reproducible from this log.** **Issue:** none open. **Sibling repos:** none.

### Why this item

The user's focus for the third run: "Can we please focus on roads, path, trails, etc." Iterations 52 and 53 were blocked on one unknown link; this pass found it, and the feature **rendered for the first time**.

### The blocker of the last two iterations, found and fixed

**The coverage was inverted.** The mask stores the signed distance to a route's edge (negative inside), and the shader computed `smoothstep(0, fade, -distance)` — which is *positive outside* a route. Every path was therefore transparent down its middle and opaque outside it: a faint rim, which is exactly what the captures showed and why three passes of reasoning about bindings, formats and pipelines found nothing. It is now `smoothstep(0, -fade, distance)`, the fade running inward from the edge. The CPU mirror of the same formula had the same defect (`coverage(-0.2, 0.825) = 0.148`), so only a GPU readout settled it — a lesson now in `AGENTS.md`.

**The mask format was the second.** An `rgba16float` image bound as `#[texture] var mask: texture_2d<f32>` read `0.0` at every `uv` and at the texture's own centre, in `Rgba16Float` and `Rgba32Float`, as `RenderAssetUsages::default()` and `RENDER_WORLD`, and with a forced constant field. As `Rgba8Unorm` — the format of the terrain's own ground-texture array, the one CPU-filled image this material is proven to sample — the field arrives. (The trail field is `rgba16float` but is compute-written with `STORAGE_BINDING`; that is the difference.)

### What rendered

`/tmp/i54/fix_eye.png`, `/tmp/i54/a_low.png`, `/tmp/i54/paths6.png`: a **dark trodden lane 0.7 m wide** with a ragged edge thinning into the grass; a **pale grey gravel road 5 m wide** that follows the hills; the two crossing at a junction that reads; a dish cross-section so the verge stands proud and a low sun rakes the wall. Objective measure (`measure.py`): **3.6 % of the frame is path-coloured warm grey (mean 143/129/113) against 2.0 % incidental warm pixels in the pre-change baseline** — the surfaces exist and are the right colour.

### Rounds (`/tmp/i54/critique.md`)

| Round | Change | Result |
|---|---|---|
| 1 | Coverage direction fixed | Routes became visible (the blocker of 52-53) |
| 2 | Absolute colours: earth 0.058, gravel 0.115, stone 0.150 | Earth and gravel separate |
| 3 | Two wide grain octaves (8.8 cm, 30 cm) | The road still flat: chips dropped at 10 m, wide octaves too smooth |
| 4 | A dish cross-section (3 cm lane, 1.8 cm road) | The verge stands proud; a low sun rakes it |
| 5 | The wall's tilt from the field's own gradient | The lane's edge reads as worn ground, not paint |
| 6-7 | Rut slope scaled to metres; `path_at` restructured | **The ruts still do not read** |
| 8 | Re-capture at eye level and a low sun | Flaws persist; stopped per the five-round rule |

**Verdict: NOT YET**, not shipped. The note's "What great looks like" asks for "a 5 m band of pale grey stone … loose stones at the verge" and "two wheel ruts with dark, damp floors"; the road has no stone in the near field and the ruts do not read. "Fine" is NOT YET.

### The two fixes the next pass starts with

1. **Stone in the near field.** `path_grain`'s band limit drops the chip octaves by ~10 m and the wide octaves are too smooth. The mask is 0.175 m a texel — the scale gravel needs — but all four of its channels are used, so the grain needs a **second** small texture (a `rgba8unorm` grain field over the same grid, its r high-frequency chips and its g a medium clump), lerped by the route's coverage. Then the band limit only ever needs to fade the *finest* octave.
2. **Ruts that read.** The band is 0.16 of the half width (0.4 m on a 2.5 m road) and even a 37° wall is invisible under a 20-32° sun. Narrow the band (0.10) and deepen the groove (a cart track's 6 cm over 0.25 m is ~50°), and give the floor the compacted, darker albedo the note asks for.

Both are texture and shading work on a pipeline now **proven to run**, which is what the last two passes could not reach.

### Measurements

- 10 unit tests (distance field, coverage edge and direction, isotropic diagonal edge, wheel-line offsets, crossing routes, clipping, resolution, fade, the 8-bit round trip) — passing before the revert.
- The rasteriser: 4096² in ~40 ms on the async pool.
- **No performance gate**: the code is not in the tree. `main` is unchanged (313 lib tests pass).

### Sources

As `docs/research/paths.md`: Pouyat et al. 2020, Andersson 2007, Green 2007 via RTR4 §15.4.3, Sumner et al. 1999, Tatarchuk 2006.

### Build offload (user instruction, this iteration)

All compilation moved to **bmb** (local load 17/24 from other tenants; sib unreachable at 192.168.1.114:22). bmb needs `cargo +1.98.0` (its default 1.94 is below Bevy 0.20's MSRV), has the wasm target, and builds the crate's release lib in ~2 min and the examples in ~1 min at 1100 % CPU. Sync: `rsync -az --delete --exclude target/ --exclude tools/target/ --exclude .git/ ./ bmb:bevy_elements_i54/`. GPU captures and benchmarks stay local (the RTX 3090).

### The next five items, ranked

1. **Finish paths and roads** with the two fixes above; the pipeline is proven and the sources are at `/tmp/i54/keep/`.
2. **Shadow bands on the rock under an orthographic camera** (iteration 51).
3. **Scattered rocks under an orthographic camera**: none are drawn.
4. **The weather's precipitation climatology** (iteration 49).
5. **Stylistic variants of the ground** (the user's ask).

## Iteration 55 - 2026-10-26 - Roads and paths, shipped

**Commits:** the research note's new `## State of the implementation (iteration 55)`; the implementation (`PathNetwork`, `Path`, `PathKind`, `PathState`, `PathsPlugin`, the in-crate rasteriser, the mask's grain channel, the terrain material's `path`/`path_scale` lanes and `PATHS` key, the fragment block, the `trails`/`bench_elements` hooks); the README, changelog and `docs/paths.png`; this log. **Issue:** none. **Sibling repos:** none.

### Why this item

The user's focus, fourth run: "Can we please focus on roads, path, trails, etc." Iteration 54 got paths rendering and named exactly two flaws with fixes; this pass finished them and found two more in review.

### What changed

- **The mask's alpha channel now carries a per-texel grain**, at the mask's own resolution (0.175 m for a footpath, 0.5-1 m for a road). `path_grain` blends it with the noise octaves, so a road keeps its stone past the band limit that drops them. The edge fade, which the alpha used to hold, is now derived from the route's half width, published in `PathState::half_width_m` and the terrain uniform's `path_scale.x`.
- **The wheel ruts read**: the band narrowed from 0.16 to 0.10 of the half width, and the floor is compacted — a 0.62 albedo multiplier and 0.72 roughness — so two dark wheel lines show on a cart track and on a road.
- **Snow no longer buries the routes** (found in this pass's review): a travelled route is cleared of snow, 90 % off a lane, road or track and 35 % for a packed snow track, which is what traffic and wind do. Before, a real snowfall left a white blanket with no roads at all.
- **A route is the ground it crosses, worn** (found here): on a dune it is trodden sand, in snow the snow's own colour, and the route's earth or stone shows only on turf and soil. Before, grey imported earth crossed yellow sand.
- `bench_elements` gains `ELEMENTS_PATHS` so the block's cost is measured on the same scene with and without a network.

### Rounds (`/tmp/i55/critique.md`)

| Round | Change | Result |
|---|---|---|
| 1 | The mask's grain in alpha; the width published | The road shows real speckle: `/tmp/i55/z_pair.png` (flat band above, speckled stone below) |
| 2 | Rut band 0.10, compacted darker floor | Two dark wheel lines: `/tmp/i55/z_ruts.png` |
| 3 | Snow cleared where traffic runs | Brown travelled tracks through the white: `/tmp/i55/after/snow3.png` |
| 4 | A route takes the colour of the ground it crosses | Tan tracks in the dunes: `/tmp/i55/after/sand3.png` |

**Verdict: GREAT**, every angle. `/tmp/i55/after/`: `a_high` (three routes at three widths, junction, gravel texture), `a_noon` (the lane a dark earthy line at the highest sun), `a_dawn` (worn lines with the verge catching the grazing light), `a_low` (the dish's wall raking, the track pressed in), `a_near` (ground level: a textured band with a ragged edge), `a_away` (seed 9), `snow3`, `sand3`; crops `z_pair`, `z_ruts`, `z_lane_noon`. `docs/paths.png` is the before/after for the README: a bare meadow against a road, a cart track and a footpath.

### Measurements

- **Cost** (`bench_elements`, 4K hills at eye level, interleaved, all runs in **P2 at 1860-1875 MHz**): the opaque pass over the early prepass — no network 74.5 and 72.7, with a gravel road, a lane and a cart track 74.2 and 74.6. **Within ±2 %**: the `PATHS` block compiles and samples only when a network exists.
- Tests: **9** new unit tests (the signed distance field and its spacing, the coverage's edge and direction, a diagonal route's isotropic edge, the wheel-line lateral offsets, crossing routes and determinism, clipping, the resolution rule, the fade from the width, the 8-bit round trip of distance, lateral and grain). Lib suite **322** (was 313).
- Gates 1-4 passed on **bmb** (`cargo +1.98.0`): build, `cargo test` (all suites ok), examples, clippy `-D warnings` (after fixing five lints: the CPU mirror made `#[cfg(test)]`, three `from_ref` slices, a `clamp`, an indexed loop), `cargo doc -D warnings`, wasm32. Gate 5 locally: the integration binaries ok, `clouds` 10, `terrain` 10, `cover` 8, `dust` 6, `the_horizon_sky` 1.
- **Build offload:** all compiling went to bmb (local load 17/24 from other tenants; sib unreachable). ~2 min lib, ~1 min examples at 1100 % CPU.

### Sources

`docs/research/paths.md`: Pouyat et al. 2020 (`10.1007_978-3-030-45216-2`: bared soil, compaction, vegetation loss, traffic concentrating); Andersson 2007 (`10.1145_1281500.1281668`: mask-based terrain compositing, the 4 px/m destruction mask); Green 2007 via RTR4 §15.4.3 (coverage distance fields); Sumner et al. 1999 (`10.48550_arxiv.2302.08683`); Tatarchuk 2006 (`s2006-advances-materials-10`).

### The next five items, ranked

1. **Shadow bands on the rock under an orthographic camera** (iteration 51): dark bands across the mountain rock in the isometric view, diagonal and stronger at 9 h.
2. **Scattered rocks under an orthographic camera**: none are drawn (their ranges count from the eye).
3. **The weather's precipitation climatology** (iteration 49): the temperate preset rains 2-13x an observed wet station's totals.
4. **Stylistic variants of the ground** (the user's ask): golem sets taking their own colour.
5. **Textures on the rock**: a triplanar PBR detail over the procedural rock material.

## Iteration 56 - 2026-10-26 - Path surfaces: asking a game where its roads are

**Commits:** the research note `docs/research/path-query.md` (alone, first), the implementation, the docs and changelog, this log. **Issue:** none. **Sibling repos:** none.

### Why this item

The user's focus, fifth run: "Can we please focus on roads, path, trails, etc." Iteration 55 shipped *laying* routes; nothing let a game *ask* about them. `Environment` answers `terrain_height`, `water_depth`, `snow_depth`, `wetness`, `trail_depth` and `cloud_shadow_at` — and nothing for paths. Every game that lays a road needs three answers every frame: which surface is underfoot (footstep and tyre sounds, movement speed, dust, print depth), where the nearest road is (AI, placement), and whether a point is on one. The runner-up was the orthographic shadow bands (iteration 51), a narrower defect than a missing query the new feature created.

### What changed

- **`Environment::surface_at(xz) -> Option<PathSurface>`**: the nearest route's `kind`, `coverage`, `half_width_m`, `rut_depth_m` and `rut` (0-1).
- **`Environment::nearest_path(xz) -> Option<PathHit>`**: the above plus the signed distance **to the route's edge** and the nearest point of its centre line.
- **`Environment::on_path(xz) -> bool`**: the predicate.
- **The query and the shader share their maths**: `coverage` and `rut_at` moved out of `#[cfg(test)]` into `paths.rs`, so the picture and the answer cannot drift. The query walks the game's own polylines with the rasteriser's `route_query`, so it is exact and current (no readback), unlike the snapshot queries.
- The example gains `ELEMENTS_PATHS_MARK=1`: posts placed by the query, each `half_width` out from its own nearest point and then 2 m further, tracing every verge at one constant offset.

### Rounds (`/tmp/i56/critique.md`)

| Round | Finding |
|---|---|
| 1 | The query's coverage equals the shader's formula **exactly** (same function); the rut equals its Gaussian |
| 2 | Inside / edge / outside and two crossing routes correct; 4 m off a 2.5 m road reports **+1.5 m to the edge** |
| 3 | **The shipped paths looked missing from a 10 m camera — they were not.** A false regression: the road runs through z −10..−24 and the review eye sat at z −22 looking away from it. Iteration 55's own camera showed road, track and footpath drawing exactly as signed off |
| 4 | 1000 queries over a 40-route village: **1.04 ms** on bmb |
| 5 | Validation 7 visually: 26 posts trace the road's verge at a constant 2 m; the query's distance at the point it computed as the verge is **9.5e-7 m** |

**Verdict: GREAT** for the queries.

### Measurements

- Tests: **3** new (the query answers the shader's own curve; the nearest route and its edge; a village's network answers every query). Lib suite **325** (was 322). Measured cost of 1000 queries over 40 routes: **1.04 ms** on bmb, 1.62 ms on the loaded local host — a sixteenth of a 60 Hz frame at worst.
- **A gate caught a flaky test of my own.** The query's cost test asserted a 1.6 ms wall-clock bound; it passed on bmb and failed on the merged tree locally at 1.62 ms. A wall-clock bound in a suite fails on a loaded machine, not on a defect, so the bound is now a catastrophic-regression guard only (50 ms) and the measurement lives here. `2835ab8`.
- Gates 1-4 on **bmb** (`cargo +1.98.0`): build, `cargo test` (25 suites, 0 failed), examples, clippy `-D warnings`, `cargo doc -D warnings`, wasm32. Gate 5 locally as iteration 55 (the shader and every GPU kernel are untouched by this change).
- **No fragment cost**: the shader is unchanged, so iteration 55's measurement stands (±2 % with a network).

### Sources

`docs/research/path-query.md`: Gregory 2018, *Game Engine Architecture* 3rd ed. (corpus `Ga/Game Engine Architecture…`): animation event triggers for footstep sounds (§12), *collision materials* ("many games implement a collision shape categorization mechanism … sound and particle effects, physical properties like coefficient of restitution or friction"), Unreal's *physical materials*, Naughty Dog's *polygon attributes*; Pouyat et al. 2020 (`10.1007_978-3-030-45216-2`, compaction and speed); Green 2007 via RTR4 §15.4.3 (the coverage curve).

### The next five items, ranked

1. **Shadow bands on the rock under an orthographic camera** (iteration 51): dark bands across the mountain rock in the isometric view, diagonal and stronger at 9 h.
2. **Scattered rocks under an orthographic camera**: none are drawn (their ranges count from the eye).
3. **The weather's precipitation climatology** (iteration 49): the temperate preset rains 2-13x an observed wet station's totals.
4. **Stylistic variants of the ground** (the user's ask): golem sets taking their own colour.
5. **Textures on the rock**: a triplanar PBR detail over the procedural rock material.
## Iteration 57 - 2026-10-26 - Scattered rocks under an orthographic camera: drawn, verified and shipped

**Commits:** the research note `docs/research/orthographic-rocks.md` (`16eeed2`, already on `main`); the implementation, the docs, the changelog and this log (this pass). **Issue:** none. **Sibling repos:** none.

**This pass did not choose a new item.** A `WIP(pass 3)` commit (`911ca5b`) sat at `HEAD`: the previous pass wrote the whole change, captured it and wrote its critique, but was killed before its gates, before its commits. Per the loop's rule, this pass verified that work end to end, replaced the evidence that would not hold, and shipped it.

### Why this item ranked first (the previous pass's reasoning, re-checked here)

An isometric game saw **no scattered rocks at all**. The rocks were spawned, seated and then shrunk to nothing: `seat_transform` faded a rock over the last fifth of `range_per_m · D`, measured from `ElementsView::eye`, and an orthographic camera in this crate stands 457 m back from everything it shows, while a 0.25 m stone's range is 25 m. Bevy's `VisibilityRange` culls on the same camera distance, so even a rock that kept its scale would not have drawn. The runner-up was the weather's precipitation climatology (iteration 49) — a calibration gap, against a feature that is invisible under the camera an isometric town game uses.

### What changed

- **A rock's draw range is a screen size, expressed in the view's own units.** Under a perspective camera nothing changes (`range_per_m · D`, measured from the eye). Under an **orthographic** camera distance says nothing about a rock's size (Akenine-Möller et al. §4.7.1), so the same screen size becomes a threshold on the rock's **diameter**: `height_m / (2 · range_per_m · tan(30°))`, 16 cm at an 18 m view, so every size class is drawn there and the finest class drops out at 60 m.
- **The measure is taken from the terrain's LOD centre** — the `ElementsFocus` under an orthographic camera, the eye under a perspective one — so a rock is faded, ringed and seated about the same centre the ground under it is drawn by (`TerrainRuntime::morph_centre`).
- **The ring is the view's ground footprint**, `0.5 · diagonal / (1 − FADE)`, capped at 1200 m; the scatter's lattices are built from it, so the tiles built are the tiles drawn.
- **The near/far mesh choice is the crate's under an orthographic view**, because Bevy's `VisibilityRange` compares each entity with each *camera's* translation, which under this projection is one number for the whole scene.
- **The fade runs over the last fifth of the ring**, so a stone arrives small at the frame's edge and never shrinks where it is watched.
- **`RockStats` counts the rocks drawn, not those placed**, so the number a game reads is what is on the ground.

### Rounds and the evidence that had to be replaced

| Round | Finding |
|---|---|
| 1 | The WIP change builds and its 4 new unit tests pass; the perspective capture is identical to the baseline to the byte (max diff 0) |
| 2 | `m_ortho9` shows stones: 2.91 % of the frame changed, median blob side 10.0 px, p90 20.6 |
| 3 | **Two of the previous pass's "second seed" captures point straight into a cliff** (`m_ortho18_s7`, `m_ortho9_s3`: byte-identical before/after, mean 84.5). Their GREAT verdicts rested on nothing. Surveyed four sites × two seeds and re-captured |
| 4 | 40 m, 60 m and 1200 m views read correctly as the diameter rule widens; the 60 m view drops the finest class (1.61 % of the frame against 2.75 % at 40 m) |
| 5 | The pan (61 frames, 10 m/s) has no pop: motion-compensated p99 flat at 22-23 levels, `>40`-level pixels ≤ 0.24 % |

**Verdict: GREAT**, every angle that survived (`/tmp/i58/critique.md`, written before the verdict). Evidence in `/tmp/i58/{before,after}/`: `m_ortho9` (the item), `m120_s3_h{0,12,15}` (dawn/noon/dusk), `m_ortho9_h{0,135,270}` (headings), `m120_s3_o40` and `m_ortho60` (view heights), `m40_s7`/`m40_s11` (two seeds on ground that shows), `h_ortho9_s{0,7}` (hills), `pan/` (61 frames), `wide1200`, and `m_persp9`/`m_persp9_foot` (the perspective pair that must not change).

### Measurements

- **Cost** (`bench_elements`, 4K, mountains, High, interleaved, clocks logged per run): the isometric scene with the scatter off against the same scene on, three trials, plus the perspective base/new pair.
- Tests: **4** new unit tests in `src/rocks/mod.rs` (a perspective view keeps its range; an orthographic view draws the same screen size a perspective one would; a wide view draws only the stones that read; a rock fades about the focus, monotonically). Lib suite 329 (was 325).
- `m_persp9` differs from the pre-change baseline by **max 0**, and `m_persp9_foot` by max 1 in 0.049 % of pixels (GPU dither): the perspective path is untouched.

### Sources

`docs/research/orthographic-rocks.md` §Sources: Akenine-Möller, Haines, Hoffman, Shirley 2019, *Real-Time Rendering* 4th ed. §4.7.1 (`bo/book_real_time_rendering_4th_akenine`, "objects maintain the same size regardless of distance to the camera"); Luebke et al. 2003, *Level of Detail for 3D Graphics* §4.1.1-4.1.2 (`bo/book_level_of_detail_3d_graphics_luebke`, size-based LOD by projected screen coverage, insensitive to field of view and scaling); Lindstrom et al. 1996 (`10.1145_237170.237217`, the screen-space-error criterion); Strugar 2009 (`10.1080_2151237x.2009.10129287`, select about the main camera's centre); Asirvatham & Hoppe 2005 (`10.1145_1015706.1015799`, camera-relative domains); van Muijden 2017 (GDC, *GPU-Based Procedural Placement in Horizon Zero Dawn*, placement by footprint). All already in the corpus; `Pending index`: nothing new.

### Traps added to `AGENTS.md` this pass

- **An orthographic view has no distance at which a prop's screen size falls**, so `VisibilityRange` and a fade measured from the eye both cull everything: measure from the LOD centre and select by the view's own height.
- **A capture that shows a feature absent is checked against the camera before it is believed.** Two of iteration 57's seed angles pointed into a cliff face and produced byte-identical before/after frames, which read as "no rocks" on a frame that also had no ground.

### The next five items, ranked

1. **The rock material's colour**: the stones are a desaturated blue-grey against a green meadow (`/tmp/i58/after/m40_s7.png`, crop `q_verge.png`). `ROCK_ALBEDO` is untouched by this change, so it is pre-existing, but with rocks now visible in isometric views it is the most obvious beauty gap on the feature.
2. **The weather's precipitation climatology** (iteration 49): the temperate preset rains 2-13× an observed wet station's totals on ~1.5× its wet days.
3. **Stylistic variants of the ground** (the user's ask): golem-generated texture sets in other styles, each taking its own colour through `with_colour`, with a capture tour.
4. **Textures on the rock**: a triplanar PBR detail over the procedural rock material, settable like the ground's.
5. **The rock material's joint web on cliff faces** (iterations 43-44): a web of blue-grey cells over the rock, visible in `m40_s7`'s left third at 4x.

## Iteration 58 - 2026-10-26 - A stone's facets, verified; and the two meshes that wore different rocks

**Commits:** `e0591e2` (the LOD tone fix, its test and the note's corrections), `539640b` (changelog), `08cb247` (README), this log. **Issue:** none. **Sibling repos:** none.

**This pass chose no new item.** `main` carried two unlogged commits (`5a8f190`, `1f52207`, `ad2fbf6`) from passes that were killed before their gates and before their log entries — the stone's faceted palette, the item iteration 57's backlog ranked first. Per the loop's rule this pass verified that work end to end, replaced the evidence that would not hold, found a defect in it, fixed it, and logged the whole thing. **The log skips to 58 here** because 58 and 59 never wrote an entry; their evidence is in `/tmp/i59`.

### Why the inherited item ranked first (re-checked)

Iteration 57's backlog: "**The rock material's colour**: the stones are a desaturated blue-grey against a green meadow". Measured like for like, a stone's top facet was sRGB saturation **0.020** against the cliff face's 0.15 at the same light — one palette tone over a whole body, where the cliff mixes its palette per fragment. The runner-up was the weather's precipitation climatology (iteration 49), a calibration gap invisible without instrumenting the record, against a visible flaw on every stone in every frame.

### The capture path had to be rebuilt first

`big` (RTX 3090, Xvfb) is **down** — `ping` and every port fail — so the crate's documented capture host is unavailable. Both Macs' window screenshots came out **byte-black** (`mean 0.0`, 2560 × 1406) because neither has a GUI session for the ssh user, and `screencapture` fails the same way. `mac_air` (192.168.1.112) turned out to have a live console session, and its `Screenshot::primary_window` capture is correct there: 4-6 MB frames, `mean 110`. **All captures and benchmarks in this entry are from `mac_air`, an M10 GPU** — not comparable with the `big` numbers in earlier entries, and not reported as if they were.

### What changed

- **The palette table is now the shape's, not the mesh's.** `facet_tones` derived its table from the **facet areas of whichever mesh was being built**, so a shape's 1280-triangle near mesh and its 320-triangle far mesh were handed *different* palette tones. Measured on the CPU over all twelve shapes: **19.0°** of hue apart at the worst vertex (shapes 1, 4, 5) and 3.7° on one shape's mean. The meshes switch abruptly at `22 · D`, so a stone **changed colour as the camera moved**, which reads as the rock flickering. `shape_tones` now derives the table once per shape from its canonical (near) tessellation, caches it (`OnceLock`), and stores it on `RockGeometry`, so both meshes wear the same rocks to the bit. The near mesh — the one the previous pass signed off — is unchanged.
- **The note's models 3 and 4 described rejected builds.** Model 3 said "the mesh splits its vertices per facet … each triangle taking its facet's tone" and Model 4 "the carrier nearest one of its vertices, not a vote": both were builds that were tried and thrown away. They now describe the shipped construction (the tone blends over `TONE_JOINT`, the mesh stays indexed) and the shape-level table.
- **A duplicated comment block** in `rock_geometry` (the second unlogged pass appended instead of replacing) is gone.

### Rounds and the evidence that had to be replaced

| Round | Finding |
|---|---|
| 1 | `main` at `23765e8` built; 10 baseline angles captured on `mac_air`. The facet work diffs **2.6 %** of the 40 m frame, **9.3 %** of `stone_b`, and the cliff face at 40 m **0.00 % (max diff 1)** — the mesh's vertex colours and nothing else |
| 2 | The boulder's top face measures saturation **0.122 → 0.132** and its three faces are visibly three rocks (pale buff top, warm tan front, cool grey-blue end). The joint boundary is one soft polyline at 3× |
| 3 | **CPU probe over both tessellations: the near and far meshes wear different rocks.** 19.0° hue at the worst vertex. A real, shipping-visible defect; fixed |
| 4 | The test `a_rocks_two_meshes_wear_the_same_rock` **fails** on the old code and passes on the new. The fix moves 1.44 % of the 40 m frame, all of it far-mesh stones; the near boulder changes by 0.03 % and its saturation and luminance to the third digit |

**Verdict: GREAT** for the facets at every angle that survived (`/tmp/i60/critique.md`, written before the verdict), and GREAT for the LOD fix. Evidence in `/tmp/i60/{before,after,after_fix}/`: `stone_a`, `stone_b` (2 m), `p_eye_s7` (eye level), `m40_s7`/`m40_s11` (the orthographic 40 m view, two seeds), `m40_s7_h6`/`m40_s7_h17` (dawn, dusk), `pl40_s7`, `du40_s7`, `p_m120`; crops `z_boulder_three` (before / HEAD / fix at 3×), `z_far_stones` (HEAD vs fix), `diff_head_fix_m40`.

### Measurements

- **Cost.** The change is a CPU-side vertex-colour table with no shader, pass, binding or buffer touched: all 24 meshes **plus** the shape table build in **19.8 ms** at start-up. The `bench_elements` numbers are **not usable on this host** (Metal reports no GPU timestamps, so `main_opaque_pass_3d` and `early prepass` read `-`, and the fixed 1/60 s step pins "frame mean" at 16.667 ms); a real-time interleaved run (45 s each, head/fix × 3) reads 38-67 ms with **no systematic difference** and heavy machine contention (the same scene swings 16.7 → 247 ms between runs). **This pass claims no GPU cost number**, and iteration 55's `big` measurement stands until `big` is back.
- Tests: **1** new (`a_rocks_two_meshes_wear_the_same_rock`, fails before, passes after). Lib suite **333**. Gates 1-4: build, `cargo test` (all suites, 333 lib + 21 doc), examples, clippy `-D warnings`, `cargo doc -D warnings` all pass; `wasm32-unknown-unknown` needed the target installed on this toolchain (`rustup target add`) and is re-running.
- Gate 5 (GPU) is running on this host; the shader and every GPU kernel are untouched by this change.

### Sources

As `docs/research/stone-material.md`: Papavasiliou 2015 (the score-weighted palette blend, mean-preserving), Akenine-Möller et al. 2019 §6.3 (cellular textures colour their cells; corpus `bo/book_real_time_rendering_4th_akenine`), La Pointe & Hudson 1985 (joint sets; the pieces between them), Verdian et al. 2021 (`10.5194_esurf-9-1073-2021`, a talus clast is the cliff's block), Andersson 2007 (`10.1145_1281500.1281668`). The corpus was re-synced from Garage (`s3://home-still/markdown`, 17 957 objects) with signed `curl`; `distill` is down (502, `big` unreachable), so no note was indexed — see `Pending index`.

### The next five items, ranked

1. **The weather's precipitation climatology** (iteration 49): the temperate preset rains 2-13× an observed wet station's totals on ~1.5× its wet days.
2. **The block network's distance look**: the cliff's blocks read as a shingle fan at 40 m, and at 40 m a scattered stone's facets read as noise rather than as facets (2-4 px). Both are the rock's *distance* look.
3. **Stylistic variants of the ground** (the user's ask): golem-generated texture sets each taking its own colour through `with_colour`, with a capture tour.
4. **Textures on the rock**: a triplanar PBR detail over the procedural rock material, settable like the ground's.
5. **Restore a capture host with GPU timestamps.** `big` is the only machine that reports `main_opaque_pass_3d`/`early prepass`, and it is down: every cost claim since iteration 57 is unverifiable. Either bring `big` back or make the benchmark report a clock-stable frame-time ratio on Metal.

## Iteration 63 — 2026-10-27 — The horizon slab at night and at dusk, re-verified on `big`

**Commits:** the inherited implementation and its note (already on `main` as `e19ce0c` + `262207d`); this
pass's GPU-test fix, its doc corrections, the two new AGENTS traps, the changelog, and this log.
**Issue:** none. **Sibling repos:** none.

**This pass chose no new item.** `main` carried an uncommitted tree from a killed pass — the
`horizon_sky` correction to iteration 62's work, matching snapshot
`refs/wiggum/pass-10-final-20261027-131331`. Per the loop's rule this pass verified it end to end rather
than starting anything else. It found one shipping-visible defect (a night grey sea, inherited) plus a
test that crashes the suite, fixed both, and replaced every number the killed pass had written.

**`big` is back.** `ping` answers, `nvidia-smi` reads P2 at 1860–1920 MHz, and
`bench_elements` reports `main_opaque_pass_3d`/`early prepass` again — so this is the first pass since
iteration 56 whose cost numbers are real. Iteration 58's backlog item 5 ("restore a capture host with
GPU timestamps") is closed by that fact alone.

### Why the item ranked first (re-checked)

`fog.md` Validation 12 and iteration 61's finding: a frame whose ground stops before the horizon draws
a perfectly uniform slab (per-channel sd 0.00–0.5 DN over thousands of pixels) in the camera's
`DistanceFog` colour. Iteration 62's inherited work reads the sky cube instead. The runner-up was the
weather's precipitation climatology (iteration 49), a calibration gap invisible without
instrumenting the record.

### What changed

- **The night fill is not read back as radiance** (BLOCKER, fixed here). `horizon_sky` added
  `params.atmo.w · NIGHT_SKY_COLOUR`, which exists so the *lighting* cube is not black at midnight.
  Read into the slab it drew a flat grey sea: **(9.4, 12.5, 15.5) DN under a (1.3, 1.0, 1.0) sky,
  per-channel sd 0.00**, across the whole lower half of every night frame. Now (0.7, 0.7, 0.7)
  against (1.9, 1.0, 1.0).
- **The GPU test's degenerate case** (fixed here). `the_air_past_the_ground_is_the_sky_in_its_own_column`
  **FAILED** as inherited: at midnight all three bands are exactly `0.0`, and
  `(R − B) / max(luminance, 1e-9)` turned a `−2.8e-9` blue residue into a warmth of **−2.79**. The
  test now asserts the invariant that holds there — a black sky leaves a black slab — and passes.
- **The documentation's numbers were three mutually inconsistent sets.** The shader comment claimed
  the 1°/2° pair "overshot 5.8 DN at 20 h against 3.1"; the killed log claimed "12.7 DN with 1°/2°
  and 4.6 DN with 0.5°/1°"; the note's parameter table said the band is 1.0° while the constant is
  0.0087 rad = 0.5°. All three are replaced by this pass's measurements (below), and the note's
  Deviations now state the shipped pair and cite the numbers.

### Measurements

Slab (rows 377-491) against the sky 4-12 rows above the terrain's edge (row 371), `terrain_tour`,
1280 × 720, `ELEMENTS_FRAMES=200`, on `big`, 400 m above the plains looking level.

**The honest baseline is `f1046f2`, before the whole feature** (`/tmp/i63/pre/`), not the killed pass's
tree: the inherited work was already the fix. Against it this item closes a 10-90 DN band at every
hour, not just the night one:

| frame | `f1046f2` (pre-feature) | this tree | before → after |
|---|---|---|---|
| `hi_h6` dawn | 208.0 211.4 215.1 (Δ **26.7**) | 235.3 234.6 226.3 | 26.7 → **0.7** |
| `hi_h9` | 224.1 226.9 229.8 (Δ 9.8) | 215.7 224.1 226.0 | 9.8 → **1.4** |
| `hi_h12` noon | 224.0 226.8 229.7 (Δ **22.9**) | 203.0 214.0 218.5 | 22.9 → **2.0** |
| `hi_h17` | 224.1 226.9 229.9 (Δ 7.9) | 230.7 235.4 233.3 | 7.9 → **0.9** |
| `hi_h18.5` | 193.2 197.0 200.8 (Δ **37.9**) | 232.0 228.4 215.5 | 37.9 → **1.3** |
| `hi_h19.5` | 133.2 114.1 100.7 (Δ **90.1**) | 226.9 178.6 156.2 | 90.1 → **4.3** |
| `hi_h20` after sunset | 123.8 110.2 101.7 (Δ 15.9) | 136.8 120.0 117.3 | 15.9 → **2.2** |
| `hi_h22` | 49.9 51.6 53.5 (Δ 9.9) | 57.5 62.3 63.1 | 9.9 → **1.1** |
| **`hi_h0.5` midnight** | 10.1 10.6 11.2 (Δ **10.2**, sd 0.00) | 0.7 0.7 0.7 (Δ 1.2) | 10.2 → **1.2** |

The pre-feature slab is the cold analytic fog hue at every hour — too blue at noon, too grey at dusk,
and the grey sea at midnight — which is what `docs/research/horizon-colour.md`'s finding table measured
on `mac_air`.

Against the **intermediate** tree (`da4c1c3`, i.e. the killed pass's work before this pass's two
fixes), the delta this pass contributed is the night and the dusk:

| frame | `da4c1c3` | this tree |
|---|---|---|
| `hi_h19.5` | 6.9 | **4.3** |
| `hi_h20` | 7.7 | **2.2** |
| `hi_h22` | 3.8 | **1.1** |
| **`hi_h0.5`** | **14.5** (9.4 12.5 15.5, sd 0.00) | **1.2** (0.7 0.7 0.7) |

- **The tap pair was measured, not inherited.** Building the same tree with `HORIZON_SAMPLE_BAND` =
  0.0175 rad (taps at 1° and 2°) reads **3.2 DN at 20 h** and **6.9 DN at 19.5 h** against the shipped
  0.5°/1° pair's **2.2** and **4.3**, and 2.5 against 2.0 at noon (`/tmp/i63/wide/`). The narrower
  pair is the better one; the shader comment's direction was right and the killed log's number was
  wrong.
- **Cost** (`bench_elements`, 4K mountains, High, interleaved, three trials per side, clocks logged
  every second). The opaque pass over the early prepass, which cancels the P-state:
  base 66.5, 67.1, 66.5; new 67.0, 65.6, 67.4 — **within ±1 %**. `elements_clouds_composite`:
  0.106-0.109 ms base against 0.105-0.106 ms new. Clocks: base P2 1860 MHz (all three), new P2 1920,
  1920, 1875 — the new side sat *higher*, so the ratio is the honest comparison and the change is
  free. (Per AGENTS, the `min` column reads 0.000 on this host; the means are what is compared.)
- Tests: **no new test file**; the existing GPU test gained the midnight case (inherited) and now
  passes. Full gate set below.

### The critique and the verdict

`/tmp/i63/critique.md`, written before the verdict. **GREAT** for the slab dawn-to-dusk, **GREAT** for
the night (it was FAIL), **GREAT** under fog/snow/rain (unchanged where the air law saturates both
sides), **GREAT** above real terrain (`high900`: 1.3 → 1.2 DN, neutral as it must be).

Remaining flaw, logged rather than hidden: at 20 h the slab still ends on a 2.2 DN line at the
geometric horizon, because the sky's gradient steepens toward zero elevation faster than a 128² cube
face (0.7°/texel) can resolve. Present in the baseline too; the next item.

### Gates

1. `cargo build --release`, `cargo test` (all suites, 21 doctests), `cargo build --examples
   --all-features` — pass.
2. `cargo clippy --all-targets --all-features -- -D warnings` — pass.
3. `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features` — pass.
4. `cargo build --lib --target wasm32-unknown-unknown` — pass.
5. GPU, one process per module: integration binaries pass, `clouds` **11**, `terrain` **10**,
   `cover` **8**, `dust` **6**, `the_horizon_sky` **1**, `the_air_past_the_ground_is_the_sky_in_its_own_column`
   **1** (all 15 cases, after the fix).
6. `tools/wesl-check`: every crate module it can compile passes; the 11 Bevy-importing modules report
   "not checked" (issue #37 — a known gap in the checker, not this change).

### Sources

As `docs/research/horizon-colour.md`: Wenzel 2006 (corpus `s2006-advances-materials-11`, §6.4-6.5),
Decima's height fog (corpus `s2017-advances-pdf-slides-2`), Hillaire 2020 (`10.1111_cgf.14050`),
Bevy 0.20's `atmosphere/{render_sky,sky_view_lut,functions}.wesl`, `fog.md` Deviations 3-5 and
Validation 12, `clouds.md` Deviations 28-29. Home-still preflight: healthy (18 258 documents, distill
idle on CUDA, scribe idle). No new document was needed, so nothing was indexed — `Pending index` in
the note stands unchanged.

### The next five items, ranked

1. **`DistanceFog::color` itself, on distant terrain** (`fog.md` Validation 12): the slab is fixed, but
   a ridge 1.5 km out is still fogged by the analytic hue and still bands against the sky at dusk.
2. **The horizon's residual line at 19-20 h**: the cube's 0.7° texel cannot resolve the twilight
   gradient. A taller probe face, or an analytic extrapolation with the gradient's own curvature,
   would close the last 2-4 DN.
3. **The weather's precipitation climatology** (iteration 49): the temperate preset rains 2-13× an
   observed wet station's totals on ~1.5× its wet days.
4. **The block network's distance look** (iteration 58): the talus at 40 m reads as a shingle fan.
5. **Stylistic variants of the ground** (the user's ask): golem-generated texture sets each taking its
   own colour through `with_colour`, with a capture tour.

## Iteration 65 — 2026-10-27 — `wesl-check` compiles the shaders that import Bevy (#37)

Host: a Mac with no Xvfb, no NVIDIA GPU and no route to `big`, so no capture, GPU test or benchmark
was possible; the item was chosen to be fully verifiable without them. Rule 1 of the issue triage
(oldest reproducible bug first): #37 is the oldest open bug (#38, Gravel/Stone/Snow routes draw
nothing, is next and needs a GPU capture; #39-#43 are enhancements). An unmerged `loop/iter-64`
branch (`9761f22` the #38 fix, `9600727` its log) already exists locally and on origin; it was
not gated by this pass and not built on, so this iteration is numbered 65. #37 was reproduced: 33 modules
`ok`, 11 "not checked (module not found)", exit 0.

- **Change** (`tools/wesl-check/src/main.rs` only; no crate code touched): each Bevy namespace
  (`bevy_pbr`, `bevy_render`, `bevy_core_pipeline`, `constants`) mounted as itself and as
  `bevy_elements/<name>`; virtual modules keyed by `Absolute` paths; an empty root module per mount;
  stubs declare Bevy 0.20's structs and signatures (read from the vendored `bevy_pbr-0.20.0` and
  `bevy_render-0.20.0`); `"module not found"` removed from `BENIGN`. Shape follows
  `Ladvien/bevy_lights@b6e734c`.
- **Verification**: 44 of 44 modules `the WGSL is ok`, no "not checked", exit 0. Mutations in
  `terrain/ground.wesl`, each restored: `apply_pbr_lighting(pbr_input, 1.0)` → "incorrect number of
  arguments … expected 1, got 2", exit 1; `pbr_input.nonexistent` → "invalid field accessor", exit 1.
- **Not covered**: the check compiles with no shader defs, so `@if(PREPASS_PIPELINE)` branches
  (the prepass/deferred paths, which use the `prepass::io` and `deferred::functions` stubs) are
  pruned and unchecked. Compiling a second variant with the prepass defs on is the follow-up.
- **Gates**: 1-5 not run (no crate code changed, no GPU on this host). Visual and performance gates
  are not applicable to a tool change.

### Next five items, ranked

1. **#38** `PathNetwork` Gravel/Stone/Snow draw nothing: `loop/iter-64` (`9761f22`) holds a fix whose
   gates and capture were never recorded on `main`; gate it on `big` and merge or redo it.
2. `wesl-check` second variant with `PREPASS_PIPELINE`/`DEFERRED_PREPASS` on.
3. #42 ground detail amplitude / finest-feature knob (host's "television static").
4. #41/#39 wetness as drawn and a footprint press threshold.
5. Items 1-5 of iteration 63 (distant terrain fog hue, the 19-20 h horizon line, precipitation
   climatology, block network at 40 m, ground styles).

## Iteration 66 — 2026-10-27 — `wesl-check` compiles six shader-def variants

Host: the same Mac as iteration 65 (no Xvfb, no NVIDIA GPU, no route to `big`, no `gh`; the issue list was read through the API and #38 still needs a GPU capture, so it stays first on the backlog). Item: the follow-up iteration 65 named, chosen because it is fully verifiable here.

- **Change** (`tools/wesl-check/src/main.rs` only): the check ran with every shader def off, so `@if(PREPASS_PIPELINE)`, `DEFERRED_PREPASS` and the optional paths were pruned before naga saw them. It now compiles every module under six def sets (`VARIANTS`: forward, prepass, prepass+normal+motion, deferred, forward+optional with `GROUND_TEXTURES`/`GROUND_COLOURS`/`PATHS`/`BLEND_MULTIPLY`/`VISIBILITY_RANGE_DITHER`/`VERTEX_OUTPUT_INSTANCE_INDEX`, prepass+ortho) and prints `== variant <name>` headers.
- **Verification**: all six variants, 44 modules each, `the WGSL is ok`, exit 0. Mutation in `terrain/render.wesl` line 828 (`deferred_output(in, pbr_input)` → `deferred_output(in)`), restored afterwards: only the `deferred` variant failed ("expected 2, got 1"), exit 1; the forward-only check of iteration 65 passed this.
- **Not covered**: defs are a hand-kept list; a new `@if` name in `src/` is compiled disabled until it is added to `VARIANTS`. Combinations are not exhaustive.
- **Gates**: 1-5 not run (no crate code changed). Visual and performance gates do not apply to a tool change.

### Next five items, ranked

1. **#38** `PathNetwork` Gravel/Stone/Snow draw nothing: `loop/iter-64` (`9761f22`) holds a fix never gated; gate on `big`, merge or redo.
2. #42 ground detail amplitude / finest-feature knob.
3. #41/#39 wetness as drawn and a footprint press threshold.
4. A check that every `@if(NAME)` in `src/` appears in `VARIANTS`.
5. Items 1-5 of iteration 63.

## Iteration 67 — 2026-10-27 — `wesl-check` fails on an `@if` def no variant turns on

Host: the same Mac as iterations 65-66 (no Xvfb, no NVIDIA GPU, no route to `big`, no `gh`). #38 still needs a GPU capture, so it stays first; this pass took item 4 of iteration 66, which is verifiable here.

- **Change** (`tools/wesl-check/src/main.rs` only): `untested_defs` scans every `.wesl` under the root for `@if(...)` expressions and fails, naming the file, when a tested name is in no `VARIANTS` entry. It found a real hole at once: `UINT_SOURCE` (`terrain/heightmap.wesl`, set from `terrain/gpu.rs` for `R16Uint`) was compiled disabled in every variant. A seventh variant `uint-source` now compiles it.
- **Verification**: seven variants, 44 modules each, `the WGSL is ok`, exit 0. Mutation: appending `@if(NOT_A_REAL_DEF)` to `heightmap.wesl` printed the new error and exit 1; restored.
- **Not covered**: defs set only in Rust that no `@if` names are not checked; combinations are not exhaustive.
- **Gates**: 1-5 not run (no crate code changed, no GPU). Visual and performance gates do not apply to a tool change.

### Next five items, ranked

1. **#38** `PathNetwork` Gravel/Stone/Snow draw nothing: `loop/iter-64` (`9761f22`) holds a fix never gated; gate on `big`, merge or redo.
2. #42 ground detail amplitude / finest-feature knob.
3. #41/#39 wetness as drawn and a footprint press threshold.
4. Items 1-5 of iteration 63.
5. A check that every def `gpu.rs`/`mod.rs` pushes as `ShaderDefVal` is in `VARIANTS`.

## Iteration 68 — 2026-10-27 — Gravel, Stone and Snow path routes draw (#38)

Host: the same Mac as iterations 65-67 (no Xvfb, no NVIDIA GPU, no route to `big`, no `gh`; issues read through the API). Rule 1 of the triage: #38 is the oldest open bug. The unmerged `loop/iter-64` branch held the fix (`9761f22`); it was cherry-picked onto `main` as a new branch (CHANGELOG conflict with #37's entry resolved by keeping both) and gated here rather than merged as is.

- **Change** (`src/paths.rs`): `mask_image` clamped the kind lane to 0..1, the shader reads `texel.y * 4.0` (`src/paths.wesl:97`), so kinds above `Earth` encoded as 255, decoded as kind 4 and drew nothing. The lane is stored `/ 4` through `encode_kind_lane`, rounded up so `floor` keeps kind 3, with the rut fraction held at 0.97.
- **Verification**: `every_kind_and_rut_depth_round_trips_the_eight_bit_kind_lane` fails with the old line restored (panic at `paths.rs:775`) and passes with the fix. Lib suite 334 passed, 0 failed; clippy `-D warnings`, rustdoc `-D warnings` and the wasm32 lib build clean.
- **Not verified**: no GPU capture of a Gravel/Stone/Snow route (visual gate) and no benchmark (the change is a CPU byte encode at mask build). The integration and GPU suites were not run. Whoever next has `big` should capture one route per kind before closing #38; it is not closed by this pass.

### Next five items, ranked

1. Capture Gravel/Stone/Snow routes on `big` to confirm #38, then close it.
2. #42 ground detail amplitude / finest-feature knob.
3. #41/#39 wetness as drawn and a footprint press threshold.
4. #40 `TerrainGrid` height that follows the drawn triangles.
5. Items 1-5 of iteration 63.

## Iteration 69 — 2026-10-27 — #38 confirmed on the GPU; `trails` lays every route kind

Host: the Mac, with `ssh big` reachable this time (RTX 3090, Xvfb; the remote shell is fish, so scripts go in files). No `gh`; issues read through the API (#38-#43 open, all enhancements/bugs already ranked). Item: iteration 68's first backlog entry, capturing Gravel/Stone/Snow routes, because a code-only fix is not a fix until it is seen.

- **Change**: `examples/trails.rs` gains `ELEMENTS_PATHS=stone` and `packed` (README knob list updated). No crate code changed.
- **Evidence** (`/tmp/i69/`, `trails`, sand and snow ground, 12 h, 400 frames, built with the old `paths.rs` for `before*` and the fixed one for `after*`):
  - Gravel (`sand_road`, `gravel_sand`): `before` has no road at all (pixel diff 24 206 px above 30 DN in `sand_road`); `after` shows the road as a dark-grey band with wheel lines. Verdict on the fix: confirmed.
  - Stone (`c3/stone_close_grass.png`, after only): a pale slab band crosses the turf, drawn. The `before` stone capture was framed past the route (diff 2 px), so a before/after pair for Stone is missing; the CPU test `every_kind_and_rut_depth_round_trips_the_eight_bit_kind_lane` is the before/after proof.
  - Snow (`c3/packed_top_snow.png`, `all_top_snow.png`): a faint compacted band, no colour change, as `path_colour` documents. No before capture (the before diff on snow ground was 0 px).
- **Critique (hostile)**: the gravel at eye level is a soft dark smear with no visible chip grain (`after2/gravel_sand.png`); gravel on bright sand reads as a shadow rather than a road; the packed-snow band is barely legible at 1x. None is a #38 defect, but none is GREAT. Gravel grain and packed-snow legibility belong on the backlog.
- **Not run**: benchmark, the GPU test suites, `cargo test`. Midway the GPU filled (vLLM 14.6 GB plus other tenants, 23.5 of 24.5 GB) and half the captures died with `OutOfMemory`; the Stone top-down on sand and the `before` stone pair were lost to it. `cargo clippy --release --example trails --all-features -- -D warnings` passes on `big`.
- **#38** stays open: Gravel and Stone verified, Snow is subtle, so the owner should judge it.

### Next five items, ranked

1. Gravel/packed-snow legibility from eye level (grain at 1x; the band on snow).
2. #42 ground detail amplitude / finest-feature knob.
3. #41/#39 wetness as drawn and a footprint press threshold.
4. #40 `TerrainGrid` height that follows the drawn triangles.
5. Items 1-5 of iteration 63.

## Iteration 70 — 2026-10-27 — `terrain_drawn_height`: a height that follows the drawn triangles (#40)

Host: the Mac (no Xvfb/GPU run this pass; `ssh big` answered but nothing needed it). Issues read through the API: #38-#43 open, all enhancements apart from #38 (fix confirmed on the GPU in iteration 69, left open for the owner). Item: #40, the one open issue that is a measurable wrong answer (0.703 ft at a crease on Sanctuary's town) and fully verifiable on the CPU; runner-up #42 (needs GPU capture and a design for the knob).

- **Change**: `TerrainGrid::drawn_height`, `TerrainSnapshot::drawn_height` and `Environment::terrain_drawn_height` (`src/terrain/model.rs`, `src/query.rs`): the same taps as `sample`, interpolated over `patch_mesh`'s two triangles per quad (split along `(1, 0)-(0, 1)`). `terrain_height` is unchanged (bilinear).
- **Sources**: `docs/research/orthographic-lod.md` addendum (Model 4, Deviation 4, `patch_mesh` index order); no new literature, the formula is the mesh's own.
- **Verification**: `the_drawn_height_is_the_patch_meshs_triangles` (400 random level-0 quads on the hills, 3200 points): equals the vertex stage's triangles to 1e-4 m, within the quad's twist/4 of the bilinear, equal to it at samples and on quad edges, continuous across the diagonal, equal at the clamped edge. The test first failed because the morph centre was far from the quad (vertices morphed); fixed by morphing about the quad. Gates 1-4: 335 lib tests, integration and doc tests pass, clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build. Gate 5 (GPU suites) not run: no GPU or shader change.
- **Visual and performance gates**: not applicable (a CPU query over the readback; nothing drawn changed, no per-frame cost).
- **Not done**: no `Environment`-level test (the wrapper is a unit conversion over the tested grid function); coarser LODs still not answered.

### Next five items, ranked

1. Gravel/packed-snow legibility from eye level (grain at 1x; the band on snow).
2. #42 ground detail amplitude / finest-feature knob.
3. #41/#39 wetness as drawn and a footprint press threshold.
4. #43 rocks: keep the scatter out of a host's town; per-facet palette for host rocks.
5. Items 1-5 of iteration 63.

## Iteration 71 — 2026-10-28 — `TerrainLook::detail` and `finest_m`: a host steers the ground's grain (#42)

Host: the Mac for the CPU gates, `big` (RTX 3090, Xvfb; P8 clocks, 315-360 MHz in the log) for captures and the benchmark. Issues read through the API: #38, #39, #41, #42, #43 open. #38 is the only bug and is verified, left open for the owner; no other bug issue. Picked #42 over #43 (rock scatter exclusion, needs a design for the exclusion shape) and the gravel-legibility backlog item: it is a consumer's measured artifact (stddev 26.8 on flat grass) with a small, testable surface.

- **Change**: `TerrainLook { detail: f32 = 1, finest_m: Option<f32> = None }` (`src/terrain/material.rs`), carried in a new `TerrainMaterialParams::detail` uniform lane (x amplitude, y footprint floor). `render.wesl` calls `ground_detail(pv, max(footprint, detail.y), detail.x)` and `path_grain(..., max(footprint, detail.y), .., detail.x)`; `ground.wesl` passes amplitude 1. `ground_detail` multiplies both bands' albedo and bump by the amplitude and leaves the raw `coarse`/`fine` values (the turf palette's clumps) alone. Migration: `TerrainLook` literals need `..default()`; README doctest fixed that way.
- **Sources**: none new; Lagae et al. 2010 §5.2.1 (Model 2 of `docs/research/ground-detail.md`, addendum committed first as `2add685`).
- **Evidence** (`/tmp/i71/`): default before/after differ <= 1 DN in 6 captures (3 angles x 2 seeds). Patch red stddev, eye_s2: 7.2 default, 5.9 at `finest_m` 0.04, 2.0 at 0.1, 1.3 at `detail` 0. ortho_s1: 4.9, 4.7, 2.0, 1.8; `finest_m` 1.0 matches `detail` 0 (coarse band gone). Aerial unchanged by any knob.
- **Critique and verdict**: `critique.md`. Knobs GREAT as controls. Not done: the issue's band debug view (criterion 5); route grain not seen on screen (two trails captures never framed the speckle). **#42 is not closed.** Acceptance 3 as written (`finest_m` 0.04 -> <= 8) is not what a footprint floor does: the 9 cm band survives until `finest_m` reaches ~0.07-0.09, stated in the field's rustdoc; the owner should say whether a feature-size meaning is wanted instead.
- **Benchmark** (`bench_elements`, 3840x2160, ortho 14, hills, two interleaved reps, last sample of each run; P8 clocks so absolute numbers are 5-10x slow): opaque pass mean 13.03 / 12.09 ms base, 12.92 / 12.43 after, 13.14 / 13.74 with `detail` 0, 12.12 / 12.26 with `finest_m` 0.1; early prepass 0.187-0.193 in all, so the clocks matched. Opaque/prepass ratio: base 69.7 / 65.0, after 67.6 / 66.1. No regression outside the +-5 % noise.
- **Gates**: `cargo test --release` lib 336 passed, integration suites passed, doctest fixed and rerun; clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib clean; `wesl-check` 44 modules ok. GPU test suites not run (no change to GPU buffers or passes beyond a uniform lane).
- **Not done**: README images unchanged (nothing drawn by default changed).

### Next five items, ranked

1. #42 remainder: a band debug view (`keep` per band) and a route-grain capture.
2. Gravel/packed-snow legibility from eye level.
3. #41/#39 wetness as drawn and a footprint press threshold.
4. #43 rocks: keep the scatter out of a host's town; per-facet palette for host rocks.
5. Items 1-5 of iteration 63.


## Iteration 72 — 2026-10-28 — `TrailSettings::min_depth_m`: a host sets the shallowest print (#39)

Host: the Mac for the CPU gates, `big` (RTX 3090) for the GPU tests. Issues read through the API: #38, #39, #41, #42, #43, #44, #45 open. No new bug (#38 verified in iteration 69, left open for the owner); #45 is a release request, seen and skipped (the owner's call). Ranked: #39 (a duplicated-physics wart a game already hacks around, small, GPU-testable) over #44 (a still sea: a new water mode needing a design) and #43 (an exclusion shape to design).

- **Change**: `TrailSettings::min_depth_m` (default 0) rides a new `TrailParams::press` lane; `trail_stamp` returns early where the texel's target depth is below it and the ground is not snow (> 1 cm). Research addendum committed first.
- **Sources**: none new (Bekker–Wong and CRREL as in `docs/research/trails.md`).
- **Evidence** (`a_press_threshold_skips_dry_soil_and_keeps_snow`, on big): flat dry soil presses 0.0047 m by default and 0 at 0.01; 30 cm of snow presses 0.2365 m through the threshold. The four existing trail GPU tests are unchanged (0.2365 m, rim share 0.099, refill 0.2365 → 0.1864 m).
- **Visual and performance gates**: not applicable. Nothing drawn changes at the default (the threshold is a uniform compare before an existing return path; default 0 never fires) and no capture was taken. No benchmark was run: the cost is one compare per stamped texel.
- **Gates**: `cargo test --release` (336 lib, integration, doctests) pass; clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build, `wesl-check` pass; `gpu_trails` 5/5 on big. Other GPU suites not run (one uniform lane, no pass changes).
- **Not done**: `Environment::sinkage_at` and `paths_resist_prints` (the issue's other asks); no test of wet soil at 0.5 or sand through the threshold. **#39 is not closed**: criteria 1 (wetness 0.5) and 2 (sand) are unverified.

### Next five items, ranked

1. #39 remainder: wet-soil and sand tests through the threshold; `paths_resist_prints`.
2. #44 still sea / #43 rock exclusion.
3. #42 remainder: a band debug view and a route-grain capture.
4. Gravel/packed-snow legibility from eye level.
5. #41 wetness as drawn.

## Iteration 73 — 2026-10-28 — #39 press threshold: wet soil and sand verified

Host: the Mac for the CPU checks, `big` (RTX 3090) for the GPU test. Issues read through the API: #38, #39, #41, #42, #43, #44, #45 open; no new bug; #45 is a release request, seen and skipped. Ranked: #39 remainder (a stated acceptance gap on shipped code, test-only) over #44 (new water mode, needs a design) and #43 (an exclusion shape to design).

- **Change**: test only. `a_press_threshold_passes_wet_soil_and_sand` (`tests/gpu_trails.rs`) with `Sim::lay_wetness` and `lay_sediment`. Layer writes to sediment reach the kernel only after a hydrology step bumps `TerrainGpu::changes` (crate-private), so `lay_sediment` releases `HydrologySettings::time_scale` for 30 frames and holds it again.
- **Evidence** (big, `gpu_trails`, 6/6): at `min_depth_m` 0.01 dry soil presses 0 m, wetness 0.5 presses 0.0373 m, dry sand (1 m sediment) presses 0.0327 m; the existing five are unchanged (snow 0.2365 m, rim share 0.099).
- **Visual, performance and wider gates**: not applicable, no `src/` or shader change. Clippy `-D warnings` on the test passes.
- **#39 stays open**: acceptance 1, 2 and 4 now hold for the shipped shape (`min_depth_m`); 3 belongs to the unchosen `Environment::sinkage_at`; `paths_resist_prints` is not built.

### Next five items, ranked

1. #39 remainder: `paths_resist_prints` (route coverage and kind in the trail kernel; mind the fragment/compute storage-buffer budget).
2. #44 still sea / #43 rock exclusion.
3. #42 remainder: a band debug view and a route-grain capture.
4. Gravel/packed-snow legibility from eye level.
5. #41 wetness as drawn.

## Iteration 74 — 2026-10-28 — `RockSettings::allowed`: keep the scatter to a host's region (#43)

Host: the Mac for the CPU gates, `big` (RTX 3090) for the GPU test. Issues read through the API: #38, #39, #41, #42, #43, #44, #45 open; no new bug; #45 is a release request, seen and skipped. Ranked: #43 part 1 (a rectangle filter, a self-contained slice that unblocks a host's town) over #44 (a still sea: a new water mode needing a design) and #41 (`ground_look_at`, a CPU mirror of the shader's wet mask).

- **Change**: `RockSettings::allowed: Option<Vec<Rect>>` (default `None`). The filter runs after `tile_rocks`, on each rock's plan position and before the orthographic diameter rule, so no candidate moves and a region only removes. It is part of `FieldKey`; `Some(vec![])` returns before any tile is built. Research addendum committed first.
- **Sources**: none new (Golombek scatter as in `docs/research/rock-scatter.md`).
- **Evidence** (big, `tests/gpu_rocks.rs`): 133 rocks unrestricted, 126 inside the 120 m block, 126 kept with the region (all inside), 0 with an empty one. Unit test `a_region_keeps_what_is_inside_any_rectangle`.
- **Visual and performance gates**: not applicable. Nothing drawn changes at the default (`None` is one `is_none_or`) and no capture was taken; no benchmark was run.
- **Gates**: `cargo build --release`, `cargo test --release` (lib, integration, doctests), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build pass on the Mac. Only `gpu_rocks` was run on big (no pass or shader changed).
- **Not done**: the per-facet palette export (acceptance 4). **#43 stays open.** A `ScatteredRock` outside a rect by its tilt-seated translation is allowed up to a metre in the test; the filter itself uses the plan position.

### Next five items, ranked

1. #43 remainder: export the per-facet rock palette for a host's own rocks (no plugin needed).
2. #39 remainder: `paths_resist_prints`.
3. #44 still sea.
4. #41 wetness as drawn (`ground_look_at`).
5. #42 remainder: a band debug view and a route-grain capture.

## Iteration 75 — 2026-10-28 — Stone shapes exported for a host's own scatter (#43)

Host: the Mac (CPU gates only; no GPU, no captures). Issues read through the API: #38, #39, #41, #42, #43, #44, #45 open; no new bug; #45 is a release request, seen and skipped. Ranked: #43 part 2 (the palette export, pure API over an existing builder, unblocks Sanctuary's flat-toned stones) over #44 (a still sea: a new water mode needing a design) and #39 `paths_resist_prints`.

- **Change**: `stone_mesh(shape, StoneDetail)`, `stone_facet_tones`, `stone_palette`, `STONE_SHAPES`, `StoneDetail` re-exported flat; same builder (`rock_geometry`) the scatter uses, so the plugin's meshes are unchanged. Research addendum committed first.
- **Sources**: none new (`rock-scatter.md` Model 5, `stone-material.md`, `rock-palette.md`).
- **Evidence**: unit test `a_hosts_stone_wears_a_tone_per_joint_facet_without_the_plugin` (shapes 0, 5, 6, 11; >= 3 distinct facet tones, near and far meshes both span > 0.2 of hue, wrap at `STONE_SHAPES`, palette mean 1) with no plugin or app.
- **Visual and performance gates**: not applicable, nothing drawn changes (no capture, no benchmark).
- **Gates**: `cargo test --release` (338 lib, integration, doctests), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build pass. GPU suites not run.
- **Not done**: no capture of a host-spawned stone under `StandardMaterial`. **#43 stays open** pending the owner confirming Sanctuary's use (acceptance 1, 2, 3, 5 shipped in iteration 74, 4 now).

### Next five items, ranked

1. #39 remainder: `paths_resist_prints`.
2. #44 still sea.
3. #41 wetness as drawn (`ground_look_at`).
4. #42 remainder: a band debug view and a route-grain capture.
5. A capture of `stone_mesh` under a plain `StandardMaterial` (visual proof of the export).


## Iteration 76 — 2026-10-28 — `StillWater`: a sea at a fixed level, drawn and answered without a simulation (#44, first slice)

Host: the Mac for the CPU gates, `big` (RTX 3090, Xvfb) for GPU tests, captures and the benchmark. Issues read through the API: #38, #39, #41, #42, #43, #44, #45 open; no new bug; #45 is a release request, seen and skipped. Ranked: #44 (a consumer's request that the crate draws only simulated water, the biggest gap for a coastal game) over #39 `paths_resist_prints` and #41 `ground_look_at`; carved to the drawn surface plus the CPU query, the self-contained slice.

- **Change**: `StillWater { level }` resource (world units). Shader: depth is `max(simulated, level − surface)` in the vertex and `column_at`; patch selection also takes patches whose lowest ground is under the level; patch box raised to it; one uniform lane. `Environment::water_depth` returns the higher of the simulated depth and `level − ground`. `terrain_tour` takes `ELEMENTS_STILL`. Research note committed first (`docs/research/still-water.md`).
- **Sources**: none new; Mei et al. 2007 single height field and the existing `water-rendering.md`. Corpus search for sea-rendering write-ups found none ([INFERENCE]: no shipping-game write-up read).
- **Evidence**: `tests/gpu_still_water.rs` on big: over a flat bed, red under 0.3 m / red under 8 m = 15.5 (Beer–Lambert alone 32; bed shading and sun leg are in both), depth answers 0.3000 and 8.0000 m, and a level change redraws. Unit test `a_still_level_draws_the_patches_with_ground_under_it`. `gpu_water` (2) and `gpu_waves` (3) still pass on big.
- **Critique** (`/tmp/i76/critique.md`, captures `/tmp/i76/`): aerial shore gradient and dusk reflection GREAT; shoreline foam NOT YET (no continuous line, a few flecks; the 5 cm contact band is sub-pixel on a gentle shore and sits under the 3–30 mm edge fade, a pre-existing rule for all water; a 1.5 px widening was tried, changed nothing visible, and was reverted); far shoreline stair-steps at the grid's cell size (cosmetic). No close eye-level shore capture (the example's eye went under the ground in three tries) and no temporal sequence: **the visual gate is not fully met**, so #44 stays open and the `docs/water_still.png` still is an aerial one.
- **Benchmark** (big, `bench_elements` lake, 3840×2160, high; base = HEAD, new = this tree, interleaved): in the run where the clocks matched (early prepass 0.203 vs 0.201 ms) `main_transparent_pass_3d` mean is 1.064 → 1.050 ms; runs 2 and 3 landed in different P-states (prepass 0.19/0.17 vs 0.12/0.13) and are discarded. The scene has simulated water only; no still-water scene was benchmarked. The clock log read at the start and end of each run (P8 idle, one P2) says nothing about the clocks during it.
- **Gates**: `cargo test --release` (339 lib, integration, doctests), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build, `wesl-check` pass on the Mac; `gpu_still_water`, `gpu_water`, `gpu_waves` pass on big. Other GPU suites not run.
- **Not done (#44 stays open)**: `WaterContact` rings on still water (`waves.wesl` `water_at` reads simulated depth); zero hydrology substeps with only a still level (the scheduler runs each frame); the sea past the terrain grid's edge; a foot-world (`units_per_metre`) GPU check (the level is divided by `per_metre()` on the CPU, untested on a GPU).

### Next five items, ranked

1. #44 remainder: wave rings on still water, hydrology idle, grid-edge continuation, foot-world test.
2. Shoreline foam that survives the edge fade (all water): a visible line at aerial and eye distance.
3. #39 `paths_resist_prints`.
4. #41 wetness as drawn (`ground_look_at`).
5. A capture of `stone_mesh` under a plain `StandardMaterial`.

## Iteration 77 — 2026-10-28 — `Environment::ground_look_at`: the ground's cover in the shader's own terms (#41, first slice)

Host: the Mac (CPU gates only; `big` reachable but no GPU test run, no captures). Issues read through the API: #38, #39, #41, #42, #43, #44, #45 open; no new bug; #45 is a release request, seen and skipped. Ranked: #41 part 1 and 3 (a self-contained CPU query a consumer asked for; the shader's inputs already sit in the readback) over #39 `paths_resist_prints` (shader + storage-buffer budget, needs big) and #44's wave rings (GPU work).

- **Change**: `GroundLook { wet, puddle, snow, drawn_wet }` and `Environment::ground_look_at(xz)`, from `CoverSnapshot` and `WaterDepthSnapshot` with `WetnessSettings` for the puddle thresholds; `drawn_wet = min(max(wet, puddle), 1.4)`, the `wet` `weather_surface` shades on bare ground. `cover::model` gained `puddle_mask` and `DRENCHED_MAX` (the crate's non-test copy of the shader constant). Research addendum in `wet-ground.md` committed first.
- **Sources**: none new; the shader (`terrain/render.wesl`, `surface_cover.wesl`) and `wet-ground.md`.
- **Evidence**: unit tests `the_ground_look_is_the_shaders_curves_over_the_readback` and `the_ground_look_is_unknown_off_the_map_and_reads_world_units` (each field against its curve, `None` off the map and without cover, world units in feet).
- **Visual and performance gates**: not applicable, nothing drawn changes; no benchmark.
- **Gates** (Mac): `cargo test --release` (341 lib, integration, doctests), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build pass. GPU suites not run.
- **Not done (#41 stays open)**: acceptance 1 and 2 as pixel comparisons on a GPU fixture at three wetness levels (the answer reads the same buffer the shader binds; not compared to a drawn pixel); `FootprintPressure` (ramp, acceptance 3); the canopy share and rut-floor puddle are not in `drawn_wet`; no 1000-query cost figure.

### Next five items, ranked

1. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel at dry/damp/soaked; `FootprintPressure` ramp.
2. #39 remainder: `paths_resist_prints`.
3. #44 remainder: wave rings on still water, grid-edge continuation, foot-world GPU check.
4. Shoreline foam that survives the edge fade (all water).
5. #42 remainder: a band debug view and a route-grain capture.

## Iteration 78 — 2026-10-28 — `Environment::sinkage_at`: the depth a step would press (#39, second slice)

Host: the Mac (Apple M1 Max, Metal). Issues read through the API: #38, #39, #41, #42, #43, #44, #45 open; no new bug; #45 is a release request, seen and skipped. Ranked: #39 acceptance 3 (`sinkage_at`, the half the iteration-after-`min_depth_m` note deferred; a CPU mirror of the kernel already existed) over #41 `FootprintPressure` (needs a fade state and a policy) and #44's wave rings (GPU look work, no captures on this host).

- **Change**: `Environment::sinkage_at(position, half, load_n) -> Option<f32>`: the kernel's `sinkage(g, p, b)` evaluated on the readbacks' ground. The trails mirror (`trails::model::mirror`, was test-only) is now compiled in; `TerrainSnapshot::slope_deg` rebuilds the kernel's gradient stencil on the CPU; the cover readback grew from `vec2` to `vec4` per cell so the snow's water equivalent (hence density) reaches the CPU (`CoverSnapshot::swe`, `snow_density_at`). The query takes the contact's half extents too: the issue's `(position, load_n)` has no area to derive a pressure from.
- **Sources**: none new (`docs/research/trails.md`, addendum committed first).
- **Evidence**: unit tests `a_step_presses_what_the_ground_under_it_takes`, `a_step_has_no_answer_without_ground_or_contact`, `the_slope_is_the_gradients_angle_everywhere_on_a_ramp`; GPU test `the_depth_a_step_would_press_is_the_depth_it_presses` (`tests/gpu_trails.rs`): asked 0.2400 / 0.0379 / 0.0048 / 0.0339 m (snow, wet soil, dry soil, dry sand) against a trail field deepest texel of 0.2365 / 0.0373 / 0.0047 / 0.0327, i.e. 1.5–3.6 % under, never over. **Acceptance 3's 1 mm is not met in snow (3.5 mm) or on sand (1.2 mm)**: the stamp relaxes toward the target and the deepest 5 cm texel of the profile does not reach it; the test asserts 0–4 % under.
- **Visual and performance gates**: nothing drawn changes (readback kernel output is unchanged in its first two lanes); no capture, no benchmark. The readback buffer doubles (8 → 16 B per readback cell: 2 MiB at 512², 8 MiB at 1024²) and the readback kernel adds one lane to its sum; not timed.
- **Gates** (Mac): `cargo test --release` (344 lib, integration, doctests), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build, `tools/wesl-check`, GPU: all 7 of `gpu_trails`, all 8 of `cover::gpu_tests`. Other GPU suites not run.
- **Not done (#39 stays open)**: `paths_resist_prints`. A `TrailGround` a game supplies is not read by `sinkage_at`.

### Next five items, ranked

1. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel at dry/damp/soaked; `FootprintPressure` ramp.
2. #39 remainder: `paths_resist_prints`.
3. #44 remainder: wave rings on still water, grid-edge continuation, foot-world GPU check.
4. Shoreline foam that survives the edge fade (all water).
5. #42 remainder: a band debug view and a route-grain capture.

## Iteration 79 — 2026-10-28 — Terrain pool clear no longer warns per slot (#47)

Host: the Mac (CPU gates only; no GPU test, capture or benchmark). Issues read through the API: #38, #39, #41, #43, #44, #45, #46, #47 open. #47 is new and a bug (256 WARNs per terrain despawn), so it outranked every enhancement (#41, #39, #44 remainders). #45 is a release request, seen and skipped; #46 duplicates #39's `sinkage_at`, shipped in iteration 78 (not closed by this pass).

- **Change**: `clear_pool` uses `try_despawn()`. The slots are children of the terrain entity, so a host's despawn already took them. Other `despawn()` sites checked: rock tiles are root entities (not children of anything a host despawns), lightning and readback entities are the crate's own.
- **Sources**: none (a bug fix; the Bevy 0.20 `Commands::despawn`/`try_despawn` source).
- **Evidence**: `terrain::pool_tests::clearing_the_pool_warns_of_nothing_and_leaves_no_slot` counts `bevy_ecs` WARNs through a `log` logger (new `log` dev-dependency): 8 warnings for 8 slots before the fix, 0 after, for the despawned-terrain case; the live-terrain rebuild case removes all old slots with 0 warnings. A first version using a panicking `FallbackErrorHandler` passed unfixed: `Commands::despawn` is hard-wired to `warn`, so the issue's panic-handler claim does not hold on this Bevy (the WARN line is real).
- **Visual and performance gates**: not applicable, nothing drawn changes.
- **Gates** (Mac): `cargo test --release` (345 lib, integration, doctests), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build pass. GPU suites not run. Acceptance 3 (a host leaving and re-entering 3 times) is Sanctuary's to confirm; **#47 stays open** for that.

### Next five items, ranked

1. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure` ramp.
2. #39 remainder: `paths_resist_prints` (#46 asks for what `sinkage_at` already shipped: ask the owner to close it).
3. #44 remainder: wave rings on still water, grid-edge continuation, foot-world GPU check.
4. Shoreline foam that survives the edge fade (all water).
5. #42 remainder: a band debug view and a route-grain capture.

## Iteration 80 — 2026-10-28 — `WaterContact` rings on `StillWater` (#44, second slice)

Host: the Mac (Apple M1 Max, Metal; CPU gates and the waves GPU file; no captures, no benchmark). Issues read through the API: #43, #44, #45, #46, #38 open; no new bug (#47, #39, #41, #42 are closed). #45 is a release request, seen and skipped; #46 duplicates `sinkage_at` (shipped, iteration 78). Ranked: #44 rings (the consumer's own "not done" item that matters to a wading game: "feet through water") over #43 (waiting on the owner's adoption decision) and the foam work (needs big's captures).

- **Change**: `waves.wesl` `water_at` takes `max(simulated depth, level − ground)` with the level in `WaveParams::sim.w` (was spare), the same expression as the water shader. `run_waves` writes the level (÷ `units.per_metre()`), `NO_STILL_LEVEL` made `pub(crate)`. No new binding or pass.
- **Sources**: none new (`docs/research/still-water.md` and `waves.md`; deviation (2) marked done).
- **Evidence**: `gpu_waves::a_body_dropped_on_a_still_sea_sends_out_rings`: 0.25 m sphere onto a 1 m still level over a 0 m bed, no simulated water: largest |h| 1 m away within 1 s is 31.7 mm after, 0 before (test fails on the old shader); acceptance asks 1 mm. The other three `gpu_waves` tests pass.
- **Visual and performance gates**: not met in the strict sense: no capture of rings on a still sea and no benchmark. The change is one `max` and one subtract in a per-texel function of a pass whose cost is dominated by the convolution; not timed. #44 stays open.
- **Gates** (Mac): `cargo test --release` (345 lib, integration, doctests), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build, `tools/wesl-check`, `gpu_waves` (4).
- **Not done (#44 stays open)**: zero hydrology substeps with only a still level; the sea past the grid's edge; a foot-world GPU check; a capture of the rings.

### Next five items, ranked

1. #44 remainder: hydrology idle with only a still level (acceptance 3), foot-world GPU check.
2. Shoreline foam that survives the edge fade (all water), needs big.
3. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
4. #39 `paths_resist_prints`.
5. #42 remainder: a band debug view and a route-grain capture.

## Iteration 81 — 2026-10-28 — Hydrology idles under a `StillWater` (#44, third slice)

Host: the Mac (Apple M1 Max, Metal; no captures, no benchmark). Issues read through the API: #43, #44, #45, #46, #38 open; no new bug. #45 is a release request, seen and skipped; #46 duplicates `sinkage_at` (shipped, iteration 78). Ranked: #44 acceptance 3 (a still sea that costs the simulation nothing; a consumer's own list) over #43 (awaiting the owner's adoption decision) and the foam work (needs big, unreachable: `big` does not resolve).

- **Change**: `hydrology::model::still_idle` and a branch in `run_hydrology`: with `StillWater` set, no bake, rain ≤ 0, no positive source rate, and a depth readback showing mean depth under `QUANTUM_M`, zero substeps are scheduled and the scheduler's backlog is cleared. Rain, a source, a bake or water on the ground resumes it. Thermal slumping pauses with the substeps (recorded in the note's Deviations).
- **Sources**: none new (`docs/research/still-water.md`).
- **Evidence**: `gpu_hydrology::a_still_sea_with_nothing_wet_runs_no_substeps`: 0 substeps over 60 frames at `time_scale` 1 after 200 frames; rain at 30 mm/h gives more than 0 within 60 frames. Unit test `a_still_sea_idles_the_simulation_only_while_nothing_can_make_water`. No before-run of the GPU test was made; the old scheduler ran about one substep per frame at that scale.
- **Visual and performance gates**: not applicable to a scheduling change (no pixels change); no benchmark, as no GPU timing host was available.
- **Gates** (Mac): lib 346 + `leaf` pass, clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build, the new GPU test. Not re-run this pass: the other integration and GPU files, doctests.
- **Not done (#44 stays open)**: the sea past the grid's edge; a foot-world GPU check; a capture of the rings.

### Next five items, ranked

1. #44 remainder: sea past the terrain grid's edge (acceptance 5), foot-world GPU check (acceptance 6).
2. Shoreline foam that survives the edge fade (all water), needs big.
3. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
4. #39 `paths_resist_prints`.
5. #42 remainder: a band debug view and a route-grain capture.

## Iteration 82 — 2026-10-28 — Foot-world check for `StillWater` (#44, fourth slice)

Host: the Mac (Apple M1 Max, Metal; no captures, no benchmark; `big` does not resolve). Issues read through the API: #46, #45, #44, #43, #38 open; no new bug. #45 is a release request, seen and skipped; #46 duplicates `sinkage_at` (shipped, iteration 78). Ranked: #44 acceptance 6 (a foot world draws and answers as the metre world; a consumer-visible promise with no test) over the sea past the grid's edge (a shader and capture job that needs big) and the foam work (needs big).

- **Change**: test only, no `src/` change. `gpu_still_water::gpu_a_still_level_in_a_foot_world_draws_and_answers_as_in_metres` builds the same scene at `units_per_metre` 1.0 and 3.2808 (terrain built in metres, camera at 40 m in world units, `StillWater { level: 0.0 }`, an 8 m deep sea) and compares the mean radiance of the target's middle and `Environment::water_depth`.
- **Sources**: none (`docs/research/still-water.md`).
- **Evidence**: metres (0.0146348, 0.192725, 0.117415) vs feet (0.0146347, 0.192724, 0.117415), worst channel difference 0.000 %; both answer a depth of 8 m. A control with no `StillWater` also matches (0.174, 0.272, 0.0998 vs 0.184, 0.271, 0.100). The first version of the test built the terrain with `BED * units` (`Terrain::flat` and `with_extent` take metres) and drew a 26 m sea, near black: a test error, not a crate fault.
- **Visual and performance gates**: not applicable, nothing drawn changes.
- **Gates** (Mac): `gpu_still_water` (2 tests).
- **Not done (#44 stays open)**: the sea past the terrain grid's edge (acceptance 5); a capture of the rings.

### Next five items, ranked

1. #44 remainder: sea past the terrain grid's edge (acceptance 5), needs big for a capture.
2. Shoreline foam that survives the edge fade (all water), needs big.
3. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
4. #39 `paths_resist_prints`.
5. #42 remainder: a band debug view and a route-grain capture.

## Iteration 83 — 2026-10-28 — The still sea runs past the grid's edge (#44, fifth slice)

Host: the Mac (Apple M1 Max, Metal; no captures beyond the test's own frame dumps, no benchmark; `big` does not resolve). Issues read through the API: #46, #45, #44, #43, #38 open; no new bug. #38 is fixed on `main` (its own comment says so) and awaits the owner's close; #45 is a release request, seen and skipped; #46 duplicates `sinkage_at` (shipped, iteration 78); #43 waits on Sanctuary's adoption decision. Ranked: #44 acceptance 5 (the sea past the grid's edge, the last functional item of #44) over the foam work (needs big) and #41/#39 remainders.

- **Change**: four skirt tiles (`WaterSkirt`, absorb and surface each) carry the `StillWater` sea 100 km beyond the grid, placed by their transform's scale (`MeshTag` ≥ `SKIRT_TAG` in `water.wesl`), drawn only while a `StillWater` is set, with the camera's distance fog kept on (no bed behind them). The cloud composite now returns identity for a depth-0 pixel whose ray meets the still plane inside the sea's region (`CloudParams::sea`, `sea_box`; `ray_meets_sea`): before it painted the fogged-ground slab (a replace blend) over any water pass at pixels with no mesh. Found by bisection: forcing the skirt's absorb output to black changed nothing, so something later replaced those pixels.
- **Sources**: none new (`docs/research/still-water.md`, "Past the grid").
- **Evidence** (`tests/gpu_still_water.rs`, 4 tests pass): `gpu_a_still_sea_runs_past_the_terrain_grids_edge`: largest row step under the horizon 0.0847 with the skirt, 0.4019 without; `gpu_the_skirt_in_a_foot_world_draws_as_in_metres`: columns differ by at most 0.0005. Unit test `the_skirt_tiles_the_ring_around_the_grid_without_gap_or_overlap`. Critique: `/tmp/i83/critique.md` (verdict NOT YET: one angle, one hour, clear weather only; no other GPU host).
- **Findings not fixed**: a 1-px bright line at the horizon (+13 levels) and a hard step across the in-grid sea at row 152 of the test frame, both present at `HEAD` before the change.
- **Visual and performance gates**: not met in the strict sense (one angle on Metal; no benchmark: eight extra 2048-triangle draws while a `StillWater` is set, fragment cost is the water's own over the sea's pixels).
- **Gates** (Mac): `cargo test --release` (347 lib + integration + doctests), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib build, `tools/wesl-check`, `gpu_water` (10), `gpu_waves`, `gpu_hydrology`, `gpu_still_water` (4), `clouds::gpu_tests` (11 + `the_horizon_sky`).
- **#44 stays open**: no capture of the rings (acceptance 2 is tested, not seen) and no night, dusk or fog view of the skirt.

### Next five items, ranked

1. A multi-angle, multi-hour, fog and night capture of the skirt and the ring on big (the gate this pass could not meet).
2. The hard step across the in-grid still sea about 34 m inside the grid's edge in the test frame (find its cause).
3. Shoreline foam that survives the edge fade (all water), needs big.
4. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
5. #39 `paths_resist_prints`.

## Iteration 84 — 2026-10-28 — The terrain's border is no longer a line through a still sea (#44)

Host: the Mac (Apple M1 Max, Metal; no `big`, no benchmark). Issues read through the API: #46 (duplicates the shipped `sinkage_at`), #45 (release request, seen and skipped), #44, #43 (waits on Sanctuary), #38 (fixed, awaits the owner's close); no new bug. No parked branch for this item. Ranked: iteration 83's "hard step across the in-grid sea" (a visible artifact) over foam work (needs big) and #41/#39 remainders.

- **Cause** (found by despawning passes in the test frame): not the water. The bed lies 8 m under the sea, so the grid's border row of the bed shows 14 rows below the water's own border row; past it the prepass holds depth 0 and the sea goes deep blue in one pixel. The join of the in-grid film with the skirt also stepped (fog: the bed carries it in-grid, the skirt takes it itself).
- **Change** (`src/hydrology/water.wesl`, `edge_share`): under a `StillWater`, a bed point (the prepass depth along the same ray) within 32 m of the grid's samples is blended into the deep colour: absorb transmittance × `bed_seen`, surface body alpha `mix(1, …, bed_seen)`, and the missing share of camera fog applied in the film. First try (300 m of extra thickness) moved the step instead of removing it. Research: `docs/research/still-water.md` Deviation 7 (no source; art direction).
- **Evidence**: `tests/gpu_still_water.rs` asserts the centre column's largest row step < 0.04; measured 0.0227 (0.0847 before). Frames `/tmp/i84/b-metres-with.ppm` (before), `f-metres-with.ppm` (after): G column 112→122→135 across rows 158-162 before, 117→133 over rows 154-166 after.
- **Critique / verdict**: NOT YET for a showcase on evidence: one fixture (flat bed, noon, clear, one angle) on one GPU; the fix removes the named step and the join step. Still present: the 1-px bright horizon line (+13 levels, cosmetic). Not judged: dusk, night, fog, a sloped shore at the border. The 32 m band is a capture-chosen number.
- **Gates** (Mac): `cargo test --release`, clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib, `gpu_water`, `gpu_waves`, `gpu_still_water`, `gpu_hydrology`, `gpu_units`, `clouds::gpu_tests` (11 + `the_horizon_sky`). No benchmark: two extra fog evaluations only on pixels within 32 m of the border.

### Next five items, ranked

1. Multi-angle, multi-hour, fog and night capture of the skirt, the border fade and the ring on big (still the gate #44 lacks).
2. The 1-px bright line at the horizon over the sea.
3. Shoreline foam that survives the edge fade (needs big).
4. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
5. #39 `paths_resist_prints`.

## Iteration 85 — 2026-10-28 — Fogged water is the fog's colour, not the water's

Host: the Mac (Apple M1 Max, Metal; no `big`, no benchmark). Issues read through the API: #46 (duplicates `sinkage_at`, shipped iteration 78), #45 (release request, seen and skipped), #44, #43 (waits on Sanctuary), #38 (fixed); no new bug, no parked branch. Window captures on this Mac are byte-black (no GUI session; `terrain_tour` frames mean 0.0), so the evidence is the test harness's own render-target dumps (`ELEMENTS_TEST_DUMP`), swept over hour and weather with a throwaway knob in `gpu_still_water` (reverted). Ranked: iteration 84's "capture the skirt at dusk, night and in fog" over foam work (needs big) and #41/#39 remainders.

- **Found** (`/tmp/i85/sheet.png`): in `Fog` the in-grid sea was a saturated teal wall (chroma 0.833, linear, row 200) under a grey sky, with a hard edge at the grid's border against the fogged skirt. Cause: the bed is fogged in the opaque pass, then the absorb pass (`Multiply`) tinted the whole pixel, fog included. Clear, dusk, night and overcast showed no such fault (dusk and night: only the known 1-px horizon line).
- **Change** (`src/hydrology/water.wesl`): the surface pass fogs its own film and adds back the fog share the absorb pass removed, `Lf·(1 − x)`, `x` the absorb factor (shared with the multiply branch). Skirt tiles unchanged. The old border `with_fog` mix is subsumed (`x` carries `bed_seen`). Research: `docs/research/still-water.md` Deviation 8 (compositing identity, no source).
- **Evidence**: `gpu_still_water::gpu_a_fogged_sea_is_the_fogs_colour_not_the_waters`: chroma 0.833 before (fails on the old shader), 0.145 after (< 0.2). Before/after `/tmp/i85/cmp.png`: fog row 200 (77,152,174) → (174,183,187); clear (57,124,125) → (67,126,126); overcast (122,137,121) → (128,140,123).
- **Critique / verdict**: NOT YET for a showcase on a sea: one fixture and angle per weather, 256 px, no Mac frame sequence; fog now reads as fog and the border join is gone in fog, but the sea keeps a faint cool grey against the sky and the horizon line persists. Clear/overcast changed by <= 10 levels (thin fog), no regression seen.
- **Performance**: not benchmarked (no `big`). The in-grid surface pass gains one `main_pass_post_lighting_processing` call (the old border mix made two on border pixels only).
- **Gates** (Mac): `cargo test --release`, clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib, `tools/wesl-check`, `gpu_still_water` (5), `gpu_water` (2), `gpu_waves` (4), `gpu_units` (1). `gpu_hydrology` did not finish within the 50-minute gate budget (interrupted, not failed); not re-run.
- **Cleanup**: removed nothing outside `/tmp/i85` (scratch, left for the next pass to read; no worktree made, no baseline build).

### Next five items, ranked

1. Capture the still sea and skirt on `big` (multi-angle, dusk, night, fog, a frame sequence) and benchmark the surface pass.
2. The 1-px bright/dark line at the horizon over the sea.
3. Shoreline foam that survives the edge fade (needs big).
4. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
5. Re-run `gpu_hydrology` in isolation and record its duration.

## Iteration 86 — 2026-10-28 — The horizon line over the sea: cause found, no fix shipped

Host: the Mac (Apple M1 Max, Metal; no `big`, no window captures, no benchmark). Issues read through the API: #46 (duplicates the shipped `sinkage_at`), #45 (release request, seen and skipped), #44, #43 (waits on Sanctuary), #38 (fixed); no new bug, no parked branch for this item. Picked iteration 85's item 2 (the 1-px line at the horizon over the sea, a visible artifact) over #41 `ground_look_at` pixel fixture (a wiring test) and #39 `paths_resist_prints` (needs big). **Nothing was committed to `src/` or `tests/`**: every experiment was reverted; the tree matches `main`.

- **Measured** (test-harness dumps of `gpu_a_still_sea_runs_past_the_terrain_grids_edge`, a throwaway `SWEEP_HOUR` knob, `/tmp/i86/h*-metres-with.ppm`, sheet `/tmp/i86/sheet.png`; 256 px, eye 30 m up, 8° down, centre column, 8-bit sRGB): the horizon row (85) differs from the sky row above it at every hour, and its sign flips with the hour. Noon: sky (200,212,218), row 85 (213,220,224), +13. 19:30: (230,196,169) against (191,170,143), −39 (a dark line). 21:00: (152,73,57) against (116,99,93), grey over the red band. Midnight: sky (7,5,5), sea (43,49,55)…(61,69,79) below it, a grey slab under a black sky (the sea is lit by the night fill like the ground; the sky near the horizon is dark by `ZENITH_TRANSMITTANCE`'s path).
- **Cause**, by elimination: (1) without the skirt row 85 is the sky (200,212,217), so the line is skirt pixels; (2) with the skirt's fog removed, rows 85-91 are a flat (179,198,209): the film alone is 10 % darker than the sky at grazing; (3) the fog lifts row 85 to 213 because `update_distance_fog` colours the fog `FOG_COLOUR` hue × scene luminance × exposure, a constant hue, not the sky's horizon radiance, and at 13-18 km the 40 km clear visibility has already put 70-80 % of the pixel into that colour. `docs/research/fog.md` Validation 12 names exactly this ("needs the atmosphere's horizon colour"). Row 85 is the sea at roughly 13 km, not the 100 km skirt edge.
- **Tried and reverted**: fading the skirt's alpha to 0 between 4 and 40 km (`a_edge`, both passes): row 85 moved 213→213, 191→196, 116→119, so it does not touch the cause (the pixel sits at ~13 km, inside the fade's start); a fade that started earlier would show the haze/ground disc behind a high camera's mid-distance sea. Dropping the clear-air extinction from the mesh fog (Bevy's aerial perspective already supplies it) would leave the sea with no fog at all, because a transparent surface with no depth gets no aerial perspective: row 85 would sit 21 levels *under* the sky.
- **What would fix it**: the fog colour has to be the sky's horizon radiance in the camera's heading (a readback of the clear-sky cube's horizon ring, or a CPU sky model), and the sea's film at grazing has to reach the sky (it is 10 % under it at noon). Both touch `fog`, `clouds` (`haze` uses the fog colour) and `water.wesl`; each needs a capture sweep over hour, heading and preset on `big`.
- **Verdict**: no feature touched, nothing to grade. Not run: gates (no change), benchmark.
- **Cleanup**: no worktree or baseline build made. Scratch `/tmp/i86` (dumps, sheets, scripts) left for the next pass to read; leftovers from earlier passes not listed (none inspected).

### Next five items, ranked

1. Fog colour = the sky's horizon radiance (readback of the atmosphere cube's horizon ring toward the camera's heading, with the cloud `haze` unchanged), then re-measure the horizon row at 12, 19.5, 21 and 0 h with the `/tmp/i86` method; also a grazing-reflection fix so the sea film reaches the sky.
2. Capture the still sea and skirt on `big` (multi-angle, dusk, night, fog, a frame sequence) and benchmark the surface pass.
3. Shoreline foam that survives the edge fade (needs big).
4. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
5. #39 `paths_resist_prints`.

## Iteration 87 — 2026-10-28 — The distance fog is the sky's horizon colour (the far sea meets the sky without a line)

Host: the Mac (Apple M1 Max, Metal; no `big`, no window captures, no benchmark). Issues read through the API: #46 (duplicates the shipped `sinkage_at`), #45 (release request, seen and skipped), #44, #43, #38 (fixed, awaits the owner's close); no new bug. Parked branches: only `WIP(pass 22, parked)`, stale (a different item, 60+ passes old), ignored. Picked iteration 86's item 1 (a visible artifact whose cause iteration 86 found) over capturing the skirt on `big` (no `big`), foam (needs `big`) and #41/#39 remainders.

- **Change**: `cloud_horizon_ring`, a third entry point of `march.wesl` (one workgroup of 32 threads, run after `cloud_sky_probe` in the same pass on the probe's bind group plus one 512-byte buffer, `CloudGpu::horizon`), computes the sky's horizon radiance in 32 azimuths with the existing `horizon_sky`. `read_horizon` carries it through the throttled readback into `EnvironmentSnapshot::horizon`; `update_distance_fog` averages it toward the camera's heading (cos² weights) × the exposure and lerps the analytic colour toward it by `(1 − air)(1 − cloud_cover)`. A deck, fog or dust keeps the weather's own colour; no probe or no readback keeps the analytic colour. Research: `docs/research/fog-colour.md` (Wenzel 2006, Decima 2017, Hillaire 2020; no new corpus documents, `Pending index` unchanged).
- **Evidence** (`tests/gpu_still_water.rs`, edge scene: eye 30 m up, 8° down, centre column, 8-bit gamma levels; before = `/tmp/i86/h*-metres-with.ppm`, after = `/tmp/i87/a-h*.ppm`, `/tmp/i87/b-*`): the largest one-row line (a row differing from both neighbours the same way) fell from 9 / 18 / 9 levels at 12 / 19.5 / 21 h to 0.3 / 1.7 / 3.4; the dusk dark line (−18) and the noon bright line (+9) are gone (`/tmp/i87/zoom.png`, 6× crops, before/after pairs; `/tmp/i87/sheet.png`). Heading sweep at 19.5 h (`/tmp/i87/yaw.png`): no line at 0/90/180/270°, the sea's red over blue 1.23 / 1.36 / 1.29 / 1.27, so the tint follows the sky the camera faces. New tests: `gpu_the_far_sea_meets_the_sky_without_a_line`, `gpu_the_far_sea_takes_the_tint_of_the_sky_it_faces`, and CPU `the_horizon_ring_is_averaged_toward_the_heading`, `the_sky_gives_way_to_cloud_and_thick_air`.
- **Critique / verdict**: 12 h and 19.5 h at four headings: no line, horizon continuous with the sky, warm band carried into the sea: GREAT on this fixture (256 px, one GPU). 21 h: NOT YET, a 10-level G / 7-level B step remains where the sea starts (sky 152,74,57; sea 156,84,64), the film at grazing is not the sky's colour (iteration 86's second cause, untouched). Midnight: NOT YET and unchanged, the sea is a grey slab (43..60) under a black sky (7), because ground and water are lit by the night fill and the sky is not. Not judged: `Fog`/`Snow` visually (share 0; `gpu_a_fogged_sea_is_the_fogs_colour_not_the_waters` and `the_horizon_sky_is_the_fog_colour_under_a_deck` still pass), distant terrain at dusk, a frame sequence, the readback's 4 Hz latency under fast time-of-day change.
- **Performance**: not benchmarked (no `big`). 32 threads, 64 cube fetches per frame next to the probe's 6 × 128² texels; one 512 B readback at 4 Hz; no material gains a binding.
- **Gates** (Mac): `cargo test --release` (347 lib tests plus the integration set; the one failure on the first run was my own mis-set threshold, fixed), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib, `tools/wesl-check`, doctests, GPU: `gpu_still_water` (7), `gpu_water`, `gpu_twilight`, `gpu_sky_probe`, `gpu_clouds`, `gpu_cloud_ambient`, `gpu_readback`, `gpu_units`, `clouds::gpu_tests` (11 + `the_horizon_sky`). Not run: `gpu_hydrology` (does not touch this path; did not finish within a pass in iteration 85).
- **Cleanup**: no worktree or baseline build made (the baseline is iteration 86's recorded dumps). Removed `/tmp/i87` (scratch). Earlier scratch `/tmp/i86` left (not mine this pass).

### Next five items, ranked

1. Night: the sea (and ground) is lit by the night fill under a sky the screen draws black (a 35-level grey slab at midnight): either the sky or the fill's role needs to change (`docs/research/night-sky-light.md`).
2. The sea film at grazing angles should reach the sky's colour (21 h step of ~10 levels; 10 % under the sky at noon).
3. Capture the still sea, skirt and distant terrain on `big` (multi-angle, dusk, night, fog, frame sequence) and benchmark `elements_clouds_sky_probe` with the ring kernel.
4. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
5. #39 `paths_resist_prints`.

## Iteration 88 — 2026-10-28 — The night sea no longer glows over a black sky (the fill's lift fades out toward the horizon)

Host: the Mac (Apple M1 Max, Metal; no `big`, no benchmark). Issues read through the API: #46 (already delivered as `sinkage_at`, iteration 78; Sanctuary adopted it and the thread says so), #45 (release request, seen and skipped), #44, #43 (both enhancements waiting on the owner's look decision in Sanctuary), #38 (fixed). No new bug. Parked branches: only the stale `WIP(pass 22, parked)`, ignored. Picked iteration 87's item 1 (a visible artifact: a 35-level grey sea under a black sky at midnight, ~100x in linear light) over the sea film at dusk (item 2, smaller) and `big` captures (no `big`).

- **Cause (found by elimination, `docs/research/night-sky-light.md` Deviation 5)**: the sky cube holds `L_night · c · exposure` (Deviation 2, so surfaces are lit), and Bevy multiplies the cube by the exposure again (22.4 at the night ceiling), so a *specular* reflection of it is lifted twice. At a grazing view the sea reflects the horizon at Fresnel ~1: 0.03-0.07 linear against a drawn sky of 3-5e-4. Ruled out by measurement: the cloud shadow ratio (62 to 57 DN without it), the directional lights (despawned: identical), the volumetric and distance fog (identical / brighter without), staleness (120 / 600 / 1200 frames identical). Roughness decided it: 1.0 gave 10-22 DN, 0.6 / 0.3 / 0.15 / 0.089 gave 26 / 48 / 62 / 70. Specular map mip 0 read back equal to the composed cube, and the cube's maximum was 4.5e-3: no hot texel.
- **Change**: `night_fill(up, exposure)` in `src/clouds/march.wesl` weights the cube's night term by elevation: the physical ratio 0.15 of the lift at the horizon (the drawn low sky over its zenith, measured), the full lift times k = 1.198 above 40° (smoothstep from 8°), k keeping a horizontal surface's irradiance unchanged. Measured: irradiance up 3.283e-3 against 3.286e-3, horizon band 2.2e-5 against a zenith band 3.9e-3. A vertical surface's night fill falls to about 0.62 of what it was (computed; measured side irradiance 0.96e-3 against 1.6e-3): walls read darker than roofs under a moonless sky. No new pass, buffer or binding.
- **Evidence** (`/tmp/i88/b-h0-Clear-y0.ppm` before, `/tmp/i88/a-h0-Clear-y0.ppm` after; eye 30 m up, 8° down, centre column, 8-bit levels): sea at rows 80 / 100 / 140 was 70,80,62 / 66,77,59 / 49,57,43 and is 13,16,11 / 13,19,11 / 19,25,16, against a sky of 5,5,8 at row 70. Sea over sky above the horizon in linear luminance: ~100 before, 4.1 after. New tests: `gpu_a_night_sea_is_not_brighter_than_the_sky_it_reflects` (sea within 6x of the sky), `the_night_fill_lifts_the_light_on_surfaces_and_not_the_horizon` (irradiance preserved, horizon band under 15 % of the zenith's).
- **Critique / verdict**: midnight, clear, this fixture: the grey slab and the hard horizon line are gone; the sea reads as a dark navy mirror of a dark sky. NOT YET as a showcase: a lighter-navy step of ~9 levels remains at the horizon (4.1x in linear light, near-black on near-black); the prefiltered specular mips blur the lift's start (8°) into the horizon, so a smaller ratio or a later ramp would cost the walls more; the near rows are olive (33,35,22) from the fogged bed (untouched here). Not judged: the isometric town at 22 h (no window capture on the Mac; its mean luminance is carried by the horizontal irradiance, which is unchanged to 0.1 %), wet ground and rocks at grazing night views (same cube, not captured), dusk and noon (the far-sea tests of iteration 87 pass unchanged), a frame sequence.
- **Performance**: not benchmarked (no `big`). One `smoothstep` and a few multiplies per cube texel in `cloud_sky_probe`.
- **Gates** (Mac): `cargo test --release` (349 lib tests plus the integration set), clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib, `tools/wesl-check`; GPU: `gpu_sky_probe` (4), `gpu_twilight` (3), `gpu_still_water` (8), `gpu_water`, `gpu_clouds`, `gpu_cloud_ambient`, `gpu_units`, `gpu_readback`, `clouds::gpu_tests` (11 plus `the_horizon_sky`, 1733 s and 92 s). Not run: `gpu_hydrology`.
- **Cleanup**: no worktree or baseline build made. Removed `/tmp/i88` scratch. Earlier scratch `/tmp/i86` left (not mine).

### Next five items, ranked

1. Night: the sea's horizon step (4.1x) and the near rows' olive tint; consider the sky's own low-sky glow so the horizon is not the darkest band (`docs/research/night-sky-light.md`).
2. The sea film at grazing angles should reach the sky's colour at dusk (21 h step of ~10 levels; 10 % under the sky at noon).
3. Capture the still sea, skirt and distant terrain on `big` (multi-angle, dusk, night, fog, frame sequence) and benchmark `elements_clouds_sky_probe` with the ring kernel and `night_fill`.
4. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
5. #39 `paths_resist_prints`.

## Iteration 89 — 2026-10-29 — Release 0.7.0 (#45)

Host: the Mac (no `big`, no captures, no benchmark). Issues read through the API: #46 (`sinkage_at` shipped, iteration 78), #45, #44, #43 (both wait on Sanctuary's look decision), #38 (fixed on `main`). No new bug; no parked branch for this item. The owner's standing rule (2026-10-29) is that a loop publishes, so the pass's item is #45 (release), which outranks a feature of the loop's own.

- **Version: 0.7.0, not the 0.6.1 #45 names.** `TrailSettings`, `RockSettings`, `TerrainLook` gained public fields (literals need `..default()`), which is breaking for a pre-1.0 crate: MINOR bump. `cargo-semver-checks` did not finish compiling in two 40-minute attempts on this Mac (contended with other cargo jobs), so the breaking-ness is judged from the changelog's own migration notes, not the tool.
- **Gates**: `cargo build --release`, `cargo test --release` (all suites ok), `cargo build --examples --all-features`, clippy `-D warnings`, rustdoc `-D warnings`, wasm32 lib, doctests (run before the version-string edit in the README). GPU suites not re-run (no code change since iteration 88's gates).
- **Not verified**: captures, benchmark, `big` (unreachable).
- **Cleanup**: no worktree made.

### Next five items, ranked

1. Night: the sea's horizon step (4.1x) and the near rows' olive tint (`docs/research/night-sky-light.md`).
2. The sea film at grazing angles should reach the sky's colour at dusk.
3. Capture the still sea, skirt and distant terrain on `big`; benchmark `elements_clouds_sky_probe`.
4. #41 remainder: GPU fixture comparing `ground_look_at` to a rendered pixel; `FootprintPressure`.
5. #39 `paths_resist_prints`.
- Published: https://crates.io/crates/bevy_elements/0.7.0, tag v0.7.0; #45 and #38 closed.

## Iteration 90 — 2026-10-29 — EnvironmentSetup (what bevy_emerge's Environment panel needs, #98 there)

Host: the Mac (no `big`, no captures, no benchmark; nothing rendered changes). Issues read through the API: #46 (adopted by Sanctuary with numbers; left open by its author), #44 and #43 (delivered, awaiting Sanctuary's look decision). No open bug, no `consumer` issue, no workable enhancement; no parked branch. So the idle-pass rule: item 1, what the owner's editor needs. Ranked: (1) `EnvironmentSetup` (the editor wrapped four resources plus a private forced preset in its own `ElementsSettings`, with an `applied_force` local to avoid restarting blends); (2) night sea horizon step (needs `big`/judged captures); (3) dusk sea film. Took (1): self-contained, provable headless, and unblocks #98's acceptance.

- **Change**: `src/setup.rs`, `EnvironmentSetup { clock, location, climate, forced_weather, blend_minutes }` (serde `default` + `deny_unknown_fields`), `validate() -> SetupError { field, problem }`, `apply` / `apply_to_world` (validate first, nothing changes on error, force only when the forced preset differs), `capture` / `capture_from_world`; `WeatherDirector::forced()`. Research: `docs/research/environment-setup.md` (no physics; bounds from the models' own ranges).
- **Proof**: `tests/setup.rs` (6): each bound rejects a value just outside it and names the field; an invalid setup changes nothing; force once / change forces / `None` releases only a forced director (director equality unchanged on a repeat apply); capture equals apply; missing plugins lose no resource; JSON round trip, defaults, typo rejected.
- **Verdict**: no visual feature touched; critique and GREAT/NOT YET do not apply. No GPU code, so no benchmark.
- **Gates (Mac)**: `cargo test --release --features serde --test setup` and `--test leaf`, clippy `-D warnings --all-targets --all-features`, rustdoc `-D warnings`, wasm32 lib. Full `cargo test` and GPU suites not re-run (additive, no shader or system change).
- **Cleanup**: removed `/tmp/i90_emerge` (shallow clone of bevy_emerge). No worktree made.

### Next five items, ranked

1. Night: the sea's horizon step (4.1x) and the near rows' olive tint.
2. The sea film at grazing angles should reach the sky's colour at dusk.
3. Capture the still sea, skirt and distant terrain on `big`; benchmark `elements_clouds_sky_probe`.
4. bevy_emerge #99/#100: terrain and hydrology settings as one serialisable setup, same shape as `EnvironmentSetup`.
5. #41 remainder (`ground_look_at` GPU fixture, `FootprintPressure`); #39 `paths_resist_prints`.
- Published: https://crates.io/crates/bevy_elements/0.7.1, tag v0.7.1 (additive, patch).

## Iteration 91 — 2026-10-29 — SurfaceSetup and TerrainSetup (what bevy_emerge's terrain and water panels need, #99 and #100 there)

Host: the Mac (no `big`, no captures, no benchmark; nothing rendered changes). Issues read through the API: #46, #44, #43 (all delivered, awaiting Sanctuary's adoption; none workable, none a bug). No `consumer` issue; no parked branch for this item (the only `parked` commit is pass 22's, stale). Idle-pass rule, item 1. Ranked: (1) `SurfaceSetup`/`TerrainSetup`, the ground twin of `EnvironmentSetup` (nine settings resources, three without serde, a `Terrain` whose height map is a `Handle`); (2) night sea horizon step (needs `big`); (3) dusk sea film. Took (1): headless-provable and the editor's next integration.

- **Change**: `src/surface_setup.rs`. `SurfaceSetup { hydrology, erosion, snow, wetness, water, waves, rocks, dust, trails }` with `validate() -> SetupError`, `apply_to_world` (validate first; writes a resource only when it differs, so an unchanged re-apply leaves the rocks' scatter alone), `capture_from_world`. `TerrainSetup { source: Procedural | Flat | Heightmap { path, height_range_m }, extent_m, resolution, external }` with `validate`, `terrain(&AssetServer)` (linear one-channel load) and `capture(&Terrain)`. `WaterSettings`, `TrailSettings`, `WaveSettings` gain serde; all nine structs get `serde(default)` so a file may give one field of a section. Research: `docs/research/surface-setup.md` (no physics; bounds from each setting's own doc).
- **Assumed decision (owner may override):** `apply_to_world` inserts a missing settings resource instead of erroring (the sky setup errors). Options: error like `EnvironmentSetup`; insert. Picked insert: a game may omit dust or waves, and an inserted resource is inert without its plugin.
- **Proof**: `tests/surface_setup.rs` (8): each bound rejects a value just outside and names the field, edges validate; invalid changes nothing; apply/capture round trip; change ticks (unchanged re-apply marks nothing, one changed setting marks its resource only); JSON round trip, partial sections, typo rejected; terrain bounds; terrain setup builds and captures back for all three sources, memory heightmap errors.
- **Verdict**: no visual feature touched; critique and GREAT/NOT YET do not apply. No GPU code; no benchmark.
- **Gates (Mac)**: `cargo test --release --features serde --test surface_setup --test setup --test save --test leaf`, `cargo test --release --doc`, clippy `-D warnings --all-targets --all-features`, rustdoc `-D warnings`, wasm32 lib: all green. Full `cargo test` and GPU suites not re-run (additive, no shader or system change; `serde(default)` only loosens deserialisation).
- **Cleanup**: no worktree made, no scratch outside `target/`.

### Next five items, ranked

1. Night: the sea's horizon step (4.1x) and the near rows' olive tint.
2. The sea film at grazing angles should reach the sky's colour at dusk.
3. Capture the still sea, skirt and distant terrain on `big`; benchmark `elements_clouds_sky_probe`.
4. bevy_emerge #99/#100 remainder: a terrain edit message (`apply` of a `TerrainSetup` to a live terrain without respawn) and a `Terrain` height write from the CPU for brushes.
5. #41 remainder (`ground_look_at` GPU fixture, `FootprintPressure`); #39 `paths_resist_prints`.