lecodes-cli 2.4.10 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/index.js +2510 -1035
  2. package/package.json +10 -10
  3. package/runtime/materials/decal-relief.mat +21 -46
  4. package/runtime/materials/decal.mat +41 -61
  5. package/runtime/materials/foliage-cards.mat +28 -140
  6. package/runtime/materials/foliage-impostor.mat +485 -0
  7. package/runtime/materials/foliage.mat +232 -0
  8. package/runtime/materials/grass.mat +244 -0
  9. package/runtime/materials/particles-quad.mat +5 -3
  10. package/runtime/materials/particles.mat +5 -3
  11. package/runtime/sdk-types.json +1 -1
  12. package/runtime/web/VERSION.json +3 -3
  13. package/runtime/web/assets/materials-Cs_z1zLi.bin +0 -0
  14. package/runtime/web/assets/materials-world-BtoAfznA.bin +0 -0
  15. package/runtime/web/player.js +3 -3
  16. package/runtime/web-lite/assets/{canvas-WxiusXCB.js → canvas-wFjS40IX.js} +1 -1
  17. package/runtime/web-lite/assets/creator2d-1pSkYRCn.js +1 -0
  18. package/runtime/web-lite/assets/creator2d-Ci5-pZGr.wasm +0 -0
  19. package/runtime/web-lite/assets/{pager-DpGa2uaE.js → pager-BZAO9gMC.js} +1 -1
  20. package/runtime/web-lite/assets/{plugins-CuEPHbHL.js → plugins-B7TuO_p4.js} +1 -1
  21. package/runtime/web-lite/assets/scene2d-F6T1Io7I.js +1 -0
  22. package/runtime/web-lite/assets/{tween-BnWYhJ7P.js → tween-DbrQTld4.js} +1 -1
  23. package/runtime/web-lite/assets/{video-BLj61nDx.js → video-bzR_t2i6.js} +1 -1
  24. package/runtime/web-lite/assets/{vlist-CwSYpLz-.js → vlist-Ap-F4H5J.js} +1 -1
  25. package/runtime/web-lite/features.json +18 -6
  26. package/runtime/web-lite/lite.js +6 -6
  27. package/src/commands/compile.ts +2 -1
  28. package/src/commands/hub/create.ts +100 -0
  29. package/src/commands/hub/credential.ts +32 -0
  30. package/src/commands/hub/index.ts +21 -0
  31. package/src/commands/hub/login.ts +89 -0
  32. package/src/commands/hub/status.ts +109 -0
  33. package/src/commands/index.ts +5 -1
  34. package/src/commands/level.ts +123 -0
  35. package/src/commands/lightmap.ts +41 -79
  36. package/src/commands/navmesh.ts +84 -67
  37. package/src/commands/secrets.ts +147 -0
  38. package/src/commands/update/index.ts +154 -64
  39. package/src/compile/designCompile.ts +2 -0
  40. package/src/compile/headlessBundle.ts +2 -0
  41. package/src/compile/projectCompile.ts +12 -1
  42. package/src/compile/sceneCompile.ts +2 -1
  43. package/src/dev/backendWorker.ts +6 -1
  44. package/src/dev/devServer.ts +2 -1
  45. package/src/dev/localBackend.ts +21 -14
  46. package/src/dev/projectBackend.ts +7 -3
  47. package/src/dev/watchProject.ts +2 -0
  48. package/src/hosts/desktopRenderer.ts +10 -2
  49. package/src/hosts/peerInstall.ts +7 -3
  50. package/src/hub/api.ts +220 -0
  51. package/src/hub/config.ts +53 -0
  52. package/src/hub/git.ts +148 -0
  53. package/src/platform/api.ts +21 -0
  54. package/src/project/env.ts +29 -0
  55. package/src/web/build.ts +149 -88
  56. package/runtime/web/assets/materials-CJzxzdUK.bin +0 -0
  57. package/runtime/web/assets/materials-world-BNQb0Z1l.bin +0 -0
@@ -0,0 +1,232 @@
1
+ // foliage: THE BARK — the OPAQUE material of the vegetation tier (trunks, limbs, stems; SDK `Model.load(…, { foliage })`
2
+ // gives a model's opaque glTF materials this one, its alpha-MASK materials the cards' - foliage-cards.mat for a crown,
3
+ // grass.mat for ground cover). One source per role since 2026-11-03 (the owner: a crown and a blade of grass are two
4
+ // different things and had one shader; the full per-pixel card variant this file used to compile as `foliage-masked`
5
+ // is gone, the lean cards are THE cards). The three share the wind (foliage-wind.vs) and this parameter list.
6
+ // A LIT shader with custom surface shading (2026-09-23): Filament's lights loop hands each light with its shadow to
7
+ // surfaceShading() — a Lambert with a faint GGX sheen — while the ambient is the material's own, written as emissive
8
+ // with the IBL off: the scene's sky (SH) on the bark's normal, darkened by the baked per-vertex sky visibility and the
9
+ // joints' cavity. A TREE IS A VOLUME IN THE AIR: it reads nothing from the ground — the ground's baked light under a
10
+ // tree is the tree's own shadow (2026-09-19 .. 2026-11-03 it was read here, and a pine in full sun drew black).
11
+ // • WIND in the vertex shader (foliage-wind.vs; 2026-09-23, Unity HDRP's Wind.hlsl as NatureManufacture ships it):
12
+ // the plant ROTATES about its root by an angle from a scrolling noise field sampled at the root (the bend, one per
13
+ // plant, wandering off the mean wind with the turbulence and rising under the gusts). The per-material drag /
14
+ // stiffness / lean come from the GLB (`profile`); bark never shivers. The depth/shadow variants run the same
15
+ // vertex shader, so real-time shadows sway with the trunk.
16
+ // • TOUCH: up to 8 benders (world position + radius, `materialParams.benders`). A plant whose
17
+ // footprint a bender enters LEANS AWAY AS ONE — the direction comes from the plant's origin, the
18
+ // amount grows with the vertex' height over the plant's own height — so a trunk stays straight;
19
+ // `plant.z` (from the asset's measured height, creator-gl foliageBindAsset) gives shrubs the full lean, trees none.
20
+ // • VARIATION: a per-instance hash of the origin tints base colour a little, so 3 000 copies of one
21
+ // spruce stop reading as one spruce. No per-instance parameter: copies keep sharing one material
22
+ // instance (fork lecodes 0025) and Filament's automatic instancing can still merge them.
23
+ // • FADE: `fade.xy` = the distance range over which the plant thins to nothing, and the LOD pass detaches the
24
+ // group past `fade.y`.
25
+ // The glTF-side parameters use gltfio's names (the AssetLoader fills them; an undeclared one it sets
26
+ // would be a panic) plus the foliage blocks below, which creator-gl writes on creation and —
27
+ // benders, wind — per frame (glb.cpp foliageUpdate). creator-gl writes EVERY block to every vegetation material,
28
+ // so the three declare the same list even where one of them (the bark: `look`, `leaf.x`) reads only part of it.
29
+
30
+ material {
31
+ name : foliage,
32
+ // lit + customSurfaceShading (2026-09-23): Filament runs the lights loop with each light's SHADOW (the sun's cascades
33
+ // - the crown shades its trunk), our surfaceShading() writes the bark's response; the IBL is switched off the way
34
+ // lightmap.mat does it (ambientOcclusion 0 + specularAmbientOcclusion simple) and the ambient is the material's own
35
+ shadingModel : lit,
36
+ customSurfaceShading : true,
37
+ specularAmbientOcclusion : simple,
38
+ doubleSided : true,
39
+ blending : opaque,
40
+ flipUV : false,
41
+ requires : [ uv0, uv1, tangents, color ],
42
+ variables : [ variation, radial ],
43
+ parameters : [
44
+ { type : float4, name : baseColorFactor },
45
+ { type : sampler2d, name : baseColorMap },
46
+ { type : float, name : metallicFactor },
47
+ { type : float, name : roughnessFactor },
48
+ { type : float3, name : emissiveFactor },
49
+ { type : float, name : normalScale },
50
+ { type : float, name : aoStrength },
51
+ { type : sampler2d, name : normalMap },
52
+ { type : float, name : hasNormalMap },
53
+ { type : sampler2d, name : metallicRoughnessMap },
54
+ { type : sampler2d, name : occlusionMap },
55
+ { type : sampler2d, name : emissiveMap },
56
+ // ---- foliage ----
57
+ // THE WIND (2026-09-23, the model of Unity HDRP's Wind.hlsl as NatureManufacture ships it — see the vertex
58
+ // shader): xz = direction × wind speed (m/s), y = turbulence (the random part of the wind — in the pack's
59
+ // km/h × share units, 12 in its demo: its C# converts the speed to m/s but not this, and every drag / bend in
60
+ // its materials is authored against that), w = gust scale (0..1: how strong the gusts riding the gust noise are)
61
+ { type : float4, name : wind },
62
+ // x = gust speed (the pack's km/h number, 20 in its demo: the gust pattern travels that many m/s and pushes
63
+ // by that much × gust), y = touch strength (m), z = colour variation (0..1), w = sun wrap (0..1)
64
+ { type : float4, name : sway },
65
+ // xy = fade start / end distance (m; end 0 = no fade)
66
+ { type : float4, name : fade },
67
+ // x = level-wide SHIVER multiplier on the material's shiver drag (1 = the profile as authored, 0 = none),
68
+ // y = LEAF SHADOW 0..1 — how much of a light's shadow a CARD takes (2026-09-23: a crown scatters light through
69
+ // itself, so its cards take the shadow map at ~0.6, not in full like bark; at 1 a distant crown is a dark
70
+ // silhouette whose only sunlit cards are the top layer), z = translucency 0..1 (sun through a thin leaf lit
71
+ // from behind), w = SKY AO 0..1 — how far the baked per-vertex sky visibility darkens the ambient
72
+ { type : float4, name : leaf },
73
+ // per MATERIAL, the pack's own wind profile (glTF material extras `lecodes.wind`, written by lecodes-assets
74
+ // from the Unity material's `_Drag` / `_Stiffness` / `_InitialBend` / `_ShiverDrag`; creator-gl fills a default
75
+ // by the plant's size when the GLB has none): x = drag (how much of the wind the plant takes: trees 0.1, grass
76
+ // 1-2), y = stiffness (the bend's growth along the plant: ~1 = linear angle in the distance from the root),
77
+ // z = initial bend (a permanent lean downwind, trees 1), w = shiver drag (the leaves' own motion: needles
78
+ // 0.02, grass 0.1-0.2, bark 0)
79
+ { type : float4, name : profile },
80
+ // the pack's `_ShiverDirectionality`: 0 = the leaves shiver along their normal, 1 = along the wind
81
+ { type : float, name : shiverDir },
82
+ // THE LOOK OF THE CARDS (2026-09-23, level-wide, creator-gl foliageSetLook): x = canopy 0..1 — the card's shading
83
+ // normal blends from its own plane (0) to the crown's hull normal (1: the direction from the crown's centre, from
84
+ // the vertex shader), which is what hides the flat quads: a card is lit as the part of the volume it sits in;
85
+ // y = the cards' normal-map strength (a strong map brings the planes back); z = edge fade — a card seen edge-on
86
+ // dissolves over this much of |N·V| (no paper edges on the silhouette; the colour pass only); w = coverage —
87
+ // the alpha is scaled by 1 + w × mip level so a crown does not thin out as its atlas mips away (Castaño)
88
+ { type : float4, name : look },
89
+ // the WIND NOISE, generated by creator-gl (glb.cpp foliageNoiseTexture): rgb = three tileable noise fields
90
+ // around 0.5, a = the gust field 0..1; read by world position with mips, so the trunk sees a blurred copy
91
+ { type : sampler2d, name : windNoise },
92
+ // per ASSET (creator-gl, from the GLB's bounds): x = plant height (m), y = footprint radius (m),
93
+ // z = touch multiplier (1 for a small plant, 0 for a tree)
94
+ { type : float4, name : plant },
95
+ // xyz = world position, w = radius (0 = unused)
96
+ { type : float4[8], name : benders },
97
+ // per bender: xy = travel direction on the ground (unit, or 0 when still), z = speed weight 0..1
98
+ // (creator-gl smooths the velocity over ~0.2 s) — near a plant's centre the lean follows the
99
+ // travel direction instead of the radial one, so crossing a bush never flips it (2026-09-12)
100
+ { type : float4[8], name : benderMotion }
101
+ ]
102
+ }
103
+
104
+ vertex {
105
+ #include "foliage-wind.vs"
106
+
107
+ void materialVertex(inout MaterialVertexInputs material) {
108
+ FoliageVertex f = foliageWind(material);
109
+ material.variation = vec4(f.hash, f.alive, f.tintN, f.vao);
110
+ // .w = the bark's baked CAVITY (COLOR_0.g, vertexAo.ts: short rays about the vertex normal - the limb joints), 1 = open
111
+ material.radial = vec4(f.hull, f.cavity);
112
+ }
113
+ }
114
+
115
+ fragment {
116
+ vec3 shIrradiance(const vec3 n) {
117
+ return max(
118
+ frameUniforms.iblSH[0].xyz
119
+ + frameUniforms.iblSH[1].xyz * n.y + frameUniforms.iblSH[2].xyz * n.z + frameUniforms.iblSH[3].xyz * n.x
120
+ + frameUniforms.iblSH[4].xyz * (n.y * n.x) + frameUniforms.iblSH[5].xyz * (n.y * n.z)
121
+ + frameUniforms.iblSH[6].xyz * (3.0 * n.z * n.z - 1.0) + frameUniforms.iblSH[7].xyz * (n.z * n.x)
122
+ + frameUniforms.iblSH[8].xyz * (n.x * n.x - n.y * n.y), 0.0);
123
+ }
124
+
125
+ vec3 envBrdfApprox(const vec3 f0, float roughness, float NoV) {
126
+ vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
127
+ vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
128
+ vec4 r = roughness * c0 + c1;
129
+ float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
130
+ vec2 ab = vec2(-1.04, 1.04) * a004 + r.zw;
131
+ return f0 * ab.x + ab.y;
132
+ }
133
+
134
+ // from material() for surfaceShading(): THE SHADING NORMAL - the material's own (the mesh normal with its map), not
135
+ // Filament's
136
+ vec3 gNs = vec3(0.0, 1.0, 0.0);
137
+ float gCavity = 1.0; // the joints' micro-shadow on the lights (material())
138
+
139
+ void material(inout MaterialInputs material) {
140
+ vec3 tn = vec3(0.0, 0.0, 1.0);
141
+ if (materialParams.hasNormalMap > 0.5) tn = texture(materialParams_normalMap, getUV0()).xyz * 2.0 - 1.0;
142
+ // written for matc's sake: an assignment to material.normal is what makes it emit the tangent frame
143
+ // (MATERIAL_NEEDS_TBN) that getWorldTangentFrame() below reads - without one the frame is zeros, the bark's normal
144
+ // normalised to NaN and every trunk went black (2026-09-23). The shading normal itself is the material's own (gNs)
145
+ material.normal = tn;
146
+ prepareMaterial(material);
147
+ vec4 base = texture(materialParams_baseColorMap, getUV0()) * materialParams.baseColorFactor;
148
+ vec3 v = getWorldViewVector();
149
+ mat3 frame = getWorldTangentFrame(); // T, B, N of the mesh (flipped on a back face: double-sided)
150
+ // THE BARK: the mesh normal with its map at full strength
151
+ tn.xy *= materialParams.normalScale;
152
+ vec3 ns = normalize(frame * tn);
153
+ gNs = ns;
154
+ // THE JOINTS (2026-09-23): the bark's baked cavity (limb bases, the trunk under a limb) on the ambient in full and on
155
+ // the lights by half (a crevice is in the sun's micro-shadow too), plus the normal map's own cavity - a texel
156
+ // whose normal leans hard off the surface sits in a groove of the bark - on the ambient
157
+ float cavity = variable_radial.w;
158
+ gCavity = mix(1.0, cavity, 0.5);
159
+ cavity *= mix(1.0, clamp(tn.z, 0.0, 1.0), 0.5);
160
+ // the per-instance tint: warmer/darker to cooler/lighter across the hash, scaled by `variation`
161
+ float hash = variable_variation.x;
162
+ vec3 tint = mix(vec3(0.86, 0.94, 0.72), vec3(1.06, 1.02, 1.12), hash);
163
+ base.rgb *= mix(vec3(1.0), tint, materialParams.sway.z);
164
+ // the distance fade thins the card (the masked twin's cutoff does the cutting)
165
+ base.a *= variable_variation.y;
166
+
167
+ vec2 mr = texture(materialParams_metallicRoughnessMap, getUV0()).bg;
168
+ float metallic = materialParams.metallicFactor * mr.x;
169
+ float roughness = materialParams.roughnessFactor * mr.y;
170
+ float occ = 1.0 + materialParams.aoStrength * (texture(materialParams_occlusionMap, getUV0()).r - 1.0);
171
+ highp vec3 glow = texture(materialParams_emissiveMap, getUV0()).rgb * materialParams.emissiveFactor;
172
+ float NoV = max(dot(ns, v), 1e-4);
173
+ // the baked sky visibility of the vertex (crown interior, trunk under the crown) on the ambient — the sun keeps
174
+ // its shadow map; by leaf.w (the sky AO knob), the rest stays for the sky that leaks through a crown's gaps
175
+ float ao = occ * mix(1.0, variable_variation.w, clamp(materialParams.leaf.w, 0.0, 1.0)) * cavity;
176
+
177
+ vec3 diffuseColor = base.rgb * (1.0 - metallic);
178
+ vec3 f0 = mix(vec3(0.04), base.rgb, metallic);
179
+ vec3 E = envBrdfApprox(f0, roughness, NoV);
180
+
181
+ // THE AMBIENT is the material's own (Filament's IBL is off: ambientOcclusion 0 + specularAmbientOcclusion simple,
182
+ // lightmap.mat's way), added as emissive; the lights - the sun with its SHADOW MAP, the lamps - come through
183
+ // surfaceShading() below, per light
184
+ // the scene's sky on the shading normal, lit real-time (the tree's own depth is the vertex AO in `ao`)
185
+ vec3 ambD = diffuseColor * shIrradiance(ns) * (1.0 - E) * ao * frameUniforms.iblLuminance;
186
+ vec3 ambS = E * shIrradiance(reflect(-v, ns)) * ao * frameUniforms.iblLuminance;
187
+ highp vec3 color = ambD + ambS;
188
+
189
+ material.baseColor = base;
190
+ material.metallic = metallic;
191
+ material.roughness = roughness;
192
+ material.reflectance = 0.35; // a faint sheen on the bark under grazing light (f0 ≈ 0.02; the lobe is in surfaceShading)
193
+ material.ambientOcclusion = 0.0; // Filament's IBL off (the header of lightmap.mat)
194
+ material.emissive = vec4(color + glow, 0.0);
195
+ }
196
+
197
+ // PER LIGHT (Filament's loop: the sun first, then the lamps in range), with the light's shadow in `visibility` -
198
+ // the crown shades itself and its neighbours through the sun's cascades (2026-09-23; the unlit tier before this
199
+ // read no shadow map and the sun lit every needle alike). A WRAPPED Lambert (thin leaves let light through) plus
200
+ // the TRANSLUCENCY of a leaf lit from behind: the back-lit share (the light on the far side of the card; the normal
201
+ // faces the viewer, double-sided) narrowed toward the view line into the light, as HDRP's thin-surface transmission
202
+ // does.
203
+ vec3 surfaceShading(const MaterialInputs materialInputs, const ShadingData shadingData, const LightData lightData) {
204
+ vec3 ns = gNs;
205
+ vec3 v = getWorldViewVector();
206
+ vec3 l = lightData.l;
207
+ float visibility = lightData.visibility;
208
+ float visibilityT = visibility;
209
+ float reach = lightData.colorIntensity.w * lightData.attenuation * visibility;
210
+ float reachT = lightData.colorIntensity.w * lightData.attenuation * visibilityT;
211
+ // the opaque variant (trunks, limbs, stems): a plain Lambert - wrapping the sun round a trunk lit its far side
212
+ // while the shadow map called that side shadow, a hard false terminator across the bark (2026-09-23) - plus a
213
+ // faint GGX sheen (f0 from `reflectance`, the ORM's roughness floored at 0.3: bark never sparkles)
214
+ float NoL = max(dot(ns, l), 0.0);
215
+ float through = 0.0;
216
+ vec3 spec = vec3(0.0);
217
+ if (NoL > 0.0) {
218
+ float NoV = max(dot(ns, v), 1e-4);
219
+ vec3 h = normalize(v + l);
220
+ float NoH = clamp(dot(ns, h), 0.0, 1.0), VoH = clamp(dot(v, h), 0.0, 1.0);
221
+ float a = max(shadingData.perceptualRoughness, 0.3); a *= a;
222
+ float a2 = a * a;
223
+ float dd = (NoH * a2 - NoH) * NoH + 1.0;
224
+ float D = a2 / (PI * dd * dd);
225
+ float Vis = 0.5 / max(NoL * sqrt(NoV * NoV * (1.0 - a2) + a2) + NoV * sqrt(NoL * NoL * (1.0 - a2) + a2), 1e-4);
226
+ vec3 F = shadingData.f0 + (vec3(1.0) - shadingData.f0) * pow(1.0 - VoH, 5.0);
227
+ spec = (D * Vis) * F * NoL;
228
+ }
229
+ return lightData.colorIntensity.rgb * reach * gCavity * (shadingData.diffuseColor * NoL / PI + spec);
230
+ return shadingData.diffuseColor * lightData.colorIntensity.rgb * (gCavity * (reach * NoL + reachT * through) / PI);
231
+ }
232
+ }
@@ -0,0 +1,244 @@
1
+ // grass: the material of GROUND COVER (grass, ferns, heath, the low plants a terrain is carpeted with) - the plant
2
+ // that IS the ground's surface (2026-11-03, split out of foliage-cards.mat; the owner: a crown and a grass blade are
3
+ // two different things and had one shader). It shares the wind (foliage-wind.vs) and the parameters of foliage.mat,
4
+ // and lights the way a blade of grass is lit: by THE GROUND IT STANDS ON. The ambient is the terrain's baked
5
+ // irradiance under the vertex (the sky through the canopy, the bounce) and the sun comes through the ground's baked
6
+ // share where the vertex's own sun ray meets it (a blade's foot is under it) - so grass under a pine stands in the
7
+ // pine's baked shadow like the terrain does, past the shadow map's reach too - and its base takes the colour of the
8
+ // soil (groundTint), which hides the seam with the ground. The crowns (foliage-cards.mat, foliage.mat) read nothing
9
+ // from the ground: a tree is a volume in the air, and the ground under it is in its own shadow.
10
+ // The fragment is foliage-cards.mat's LEAN one (2026-10-18): per-vertex lighting, one texture read, no derivatives.
11
+ // The engine treats it as foliage (it declares `benders`) and writes the same blocks; the SDK picks it for the MASK
12
+ // materials of a `Model.load({ foliage: 'grass' })`.
13
+
14
+ material {
15
+ name : grass,
16
+ shadingModel : lit,
17
+ customSurfaceShading : true,
18
+ specularAmbientOcclusion : simple,
19
+ doubleSided : true,
20
+ blending : masked,
21
+ alphaToCoverage : true,
22
+ flipUV : false,
23
+ requires : [ uv0, color ],
24
+ variables : [ variation, radial, lightv, baked ],
25
+ parameters : [
26
+ { type : float4, name : baseColorFactor },
27
+ { type : sampler2d, name : baseColorMap },
28
+ { type : float, name : metallicFactor },
29
+ { type : float, name : roughnessFactor },
30
+ { type : float3, name : emissiveFactor },
31
+ { type : float, name : normalScale },
32
+ { type : float, name : aoStrength },
33
+ { type : sampler2d, name : normalMap },
34
+ { type : float, name : hasNormalMap },
35
+ { type : sampler2d, name : metallicRoughnessMap },
36
+ { type : sampler2d, name : occlusionMap },
37
+ { type : sampler2d, name : emissiveMap },
38
+ // ---- foliage (foliage.mat has every comment) ----
39
+ { type : float4, name : wind },
40
+ { type : float4, name : sway },
41
+ { type : float4, name : fade },
42
+ { type : float4, name : leaf },
43
+ { type : float4, name : profile },
44
+ { type : float, name : shiverDir },
45
+ { type : float4, name : look },
46
+ { type : sampler2d, name : windNoise },
47
+ { type : float4, name : plant },
48
+ { type : float4[8], name : benders },
49
+ { type : float4[8], name : benderMotion },
50
+ // ---- THE GROUND'S LIGHT (foliage.mat's block, read PER VERTEX here; 2026-10-18, the Forest's baked ground) ----
51
+ // level-wide, written by creator-gl once a terrain's rect is bound (glb.cpp bindGround / bindOptions)
52
+ { type : float4, name : ground, precision : high },
53
+ { type : float4, name : groundInv, precision : high },
54
+ { type : float4, name : groundST, precision : high },
55
+ { type : sampler2d, name : lightmapLight, precision : high },
56
+ { type : sampler2d, name : lightmapAux },
57
+ { type : float, name : lightScale },
58
+ { type : float4, name : sunDir },
59
+ { type : float4, name : sunLight, precision : high },
60
+ // ---- THE GROUND'S COLOUR (2026-10-18, glb.cpp foliageSetGroundTint): the terrain's control map (its four layers'
61
+ // weights) and each layer's mean colour; a card's base takes the colour of the soil under it, its top its own
62
+ { type : sampler2d, name : groundControl },
63
+ { type : float4[4], name : groundTint },
64
+ { type : float4, name : groundTintOrigin, precision : high }, // xy = the terrain's world xz origin, z = bound
65
+ { type : float4, name : groundTintInv, precision : high } // the inverse of its world xz axes
66
+ ]
67
+ }
68
+
69
+ vertex {
70
+ #include "foliage-wind.vs"
71
+ #include "lightmap-sunmap.fs"
72
+
73
+ // THE GROUND'S LIGHT AT A VERTEX: true when the plant stands over the bound terrain. `ambient` = the terrain texel's irradiance there without the
74
+ // sun's part (lux, on the up normal) — the sky through the canopy, the bounce. `sunShare` = the baked share of the
75
+ // sun's disc at the point where the sun ray through `uwp` meets the ground at the plant's base height (a blade's
76
+ // foot is under it): what shades that point shades the plant — a neighbour's crown, a wall — so a
77
+ // tree past the shadow map's reach still stands in its neighbour's shadow, and the grass under a pine in the
78
+ // pine's. `lod` picks the pages' level (a far vertex over one texel of 25 cm would flicker between texels as it sways).
79
+ // the crowns' level over the ground: a vertex this high takes the open sky's ambient and its own crown's softened shadow
80
+ #define CANOPY 12.0
81
+
82
+ bool groundLightV(highp vec3 uwp, highp float baseY, float lod, out highp vec3 ambient, out float sunShare) {
83
+ ambient = vec3(0.0); sunShare = 1.0;
84
+ if (materialParams.ground.z < 0.5) return false;
85
+ highp vec2 d = uwp.xz - materialParams.ground.xy;
86
+ highp vec2 g = vec2(dot(materialParams.groundInv.xy, d), dot(materialParams.groundInv.zw, d));
87
+ if (g.x < 0.0 || g.y < 0.0 || g.x > 1.0 || g.y > 1.0) return false; // off the terrain
88
+ highp vec2 luv = g * materialParams.groundST.xy + materialParams.groundST.zw;
89
+ highp vec3 E = textureLod(materialParams_lightmapLight, luv, lod).rgb * materialParams.lightScale;
90
+ ambient = E;
91
+ if (materialParams.sunDir.w > 0.5) {
92
+ highp ivec2 auxSize = textureSize(materialParams_lightmapAux, 0);
93
+ highp int level = clamp(int(lod + 0.5), 0, max(sunMapLevels(), 1) - 1);
94
+ // the sun's part out of the ground's E under the vertex: its illuminance x the share there x the cosine on the
95
+ // ground, taken as flat
96
+ float shareHere = sunMapAt(luv, auxSize, level);
97
+ ambient = max(E - materialParams.sunLight.rgb * (shareHere * max(materialParams.sunDir.y, 0.0)), vec3(0.0));
98
+ // the sun ray through the vertex, followed down to the plant's base height
99
+ vec3 s = materialParams.sunDir.xyz; // TO the sun
100
+ highp vec3 foot = uwp - s * ((uwp.y - baseY) / max(s.y, 0.05));
101
+ d = foot.xz - materialParams.ground.xy;
102
+ g = clamp(vec2(dot(materialParams.groundInv.xy, d), dot(materialParams.groundInv.zw, d)), 0.0, 1.0);
103
+ sunShare = sunMapAt(g * materialParams.groundST.xy + materialParams.groundST.zw, auxSize, level);
104
+ }
105
+ return true;
106
+ }
107
+
108
+ vec3 shIrradianceV(const vec3 n) {
109
+ return max(
110
+ frameUniforms.iblSH[0].xyz
111
+ + frameUniforms.iblSH[1].xyz * n.y + frameUniforms.iblSH[2].xyz * n.z + frameUniforms.iblSH[3].xyz * n.x
112
+ + frameUniforms.iblSH[4].xyz * (n.y * n.x) + frameUniforms.iblSH[5].xyz * (n.y * n.z)
113
+ + frameUniforms.iblSH[6].xyz * (3.0 * n.z * n.z - 1.0) + frameUniforms.iblSH[7].xyz * (n.z * n.x)
114
+ + frameUniforms.iblSH[8].xyz * (n.x * n.x - n.y * n.y), 0.0);
115
+ }
116
+
117
+ void materialVertex(inout MaterialVertexInputs material) {
118
+ FoliageVertex f = foliageWind(material);
119
+ // COVERAGE from the distance (foliage.mat reads the atlas' mip level from the uv derivatives per pixel): the
120
+ // atlas texels under a pixel double every doubling of the distance past ~8 m, where a card is still read in
121
+ // full; `look.w` scales how much of that goes into the alpha
122
+ float lodEst = max(log2(max(f.dist, 1.0) / 8.0), 0.0);
123
+ material.radial = vec4(f.hull, lodEst);
124
+ // THE LIGHT, per vertex. Ambient = the sky's SH on the hull normal, as radiance under the camera's exposure
125
+ // (frameUniforms.iblLuminance holds it), darkened by the baked sky visibility by `leaf.w`. The sun = a wrapped
126
+ // Lambert on the hull (thin leaves let light past the terminator, `sway.w`) plus the translucency of a card lit
127
+ // from behind (`leaf.z`, narrowed toward the view line into the light; less deep in the crown), one scalar the
128
+ // fragment multiplies by the light's colour, intensity and SHADOW
129
+ float ao = mix(1.0, f.vao, clamp(materialParams.leaf.w, 0.0, 1.0));
130
+ vec3 skyAmbient = shIrradianceV(f.hull) * frameUniforms.iblLuminance;
131
+ vec3 ambient = skyAmbient;
132
+ float sunShare = 1.0;
133
+ float up = 1.0; // the vertex's height over the ground as a share of the canopy's (1 without a ground)
134
+ highp vec3 gAmb;
135
+ if (groundLightV(f.uwp, f.uOrigin.y, lodEst, gAmb, sunShare)) {
136
+ // THE BAKED GROUND'S LIGHT (the function above): its ambient on the hull normal by the hemisphere formula
137
+ // lightmapShade uses for a tilted normal (the light comes from above), as radiance E / pi under the camera's
138
+ // exposure. The crown's base takes the ground's ambient (the sky through the canopy), its top the open sky's
139
+ // SH - blended by the vertex's height over the plant; a grass blade is all ground
140
+ // the hemisphere formula alone gave a down-facing hull nothing (black needles under a young pine, the owner
141
+ // 2026-10-18): what a card sees below is the ground, lit by that same E and reflecting a quarter of it
142
+ vec3 groundAmbient = gAmb * mix(0.25, 1.0, (1.0 + f.hull.y) * 0.5) / PI * frameUniforms.exposure;
143
+ // by the height OVER THE GROUND, not within the plant: a bush's top is under the same crowns as the ground
144
+ // beside it (a 1.5 m bush with the open sky's SH on its top was a lamp in the shade - the owner, 2026-10-18);
145
+ // the open sky is reached at the crowns' level, CANOPY metres up
146
+ up = clamp((f.uwp.y - f.uOrigin.y) / CANOPY, 0.0, 1.0);
147
+ ambient = mix(groundAmbient, skyAmbient, up);
148
+ }
149
+ ambient *= ao;
150
+ vec3 l = frameUniforms.lightDirection;
151
+ vec3 v = normalize(getWorldCameraPosition() - f.wp);
152
+ float wrap = clamp(materialParams.sway.w, 0.0, 1.0);
153
+ float NoL = max((dot(f.hull, l) + wrap) / (1.0 + wrap), 0.0);
154
+ float back = max(-dot(f.hull, l), 0.0);
155
+ float vl = pow(max(dot(-v, l), 0.0), 4.0);
156
+ float through = clamp(materialParams.leaf.z, 0.0, 1.0) * back * (0.5 + 0.5 * vl) * mix(1.0, f.vao, 0.5);
157
+ material.lightv = vec4(ambient, NoL);
158
+ // .x = the baked share of the sun at this vertex (1 without a bound ground): surfaceShading takes it as a second
159
+ // shadow map on the direct light, the stricter of the two, never the product. .y = the translucency, which takes
160
+ // the shadow map ALONE: the bake's 64-ray share through the crown put out the glow of a back-lit crown (the
161
+ // multiple scattering a real crown has and this model lacks; the owner, 2026-10-18 "лес довольно тёмный")
162
+ // .z = the height share: near the ground a shadow is a NEIGHBOUR's and lands in full, at the crowns' level it is the
163
+ // crown's own and is softened by leaf.y (the scattering a real crown has); the fragment weighs the shadow by it.
164
+ // (The plants' SHADOW LOD is the LOD pass's, per renderable - lod.cpp gFoliageCast - not this shader's: a card-level
165
+ // discard in the depth variant saved 0.6 ms of the pass's 6.8, the renderables' vertices and draws are the cost)
166
+ // THE GROUND'S COLOUR at the plant's base: the layers' weights under the PLANT (one colour per plant - a varying
167
+ // that differs per vertex could not carry a packed value) times their mean colours; the fragment blends the card's
168
+ // colour toward it by `baked.w` - all of it at the ground, none from ~70 % of the plant's height up (a grass
169
+ // blade's base is its soil, a bush's crown its own leaves; a tree's cards never reach it). The colour rides
170
+ // `variation.w` packed 7 bits a channel (matc allows four variables with the colour attribute; vao was unused there)
171
+ float gtPacked = 0.0, gtBlend = 0.0;
172
+ if (materialParams.groundTintOrigin.z > 0.5) {
173
+ highp vec2 td = f.uOrigin.xz - materialParams.groundTintOrigin.xy;
174
+ highp vec2 tg = vec2(dot(materialParams.groundTintInv.xy, td), dot(materialParams.groundTintInv.zw, td));
175
+ if (tg.x >= 0.0 && tg.y >= 0.0 && tg.x <= 1.0 && tg.y <= 1.0) {
176
+ vec4 w = textureLod(materialParams_groundControl, tg, 0.0);
177
+ float ws = max(w.x + w.y + w.z + w.w, 1e-3);
178
+ vec3 soil = (materialParams.groundTint[0].rgb * w.x + materialParams.groundTint[1].rgb * w.y
179
+ + materialParams.groundTint[2].rgb * w.z + materialParams.groundTint[3].rgb * w.w) / ws;
180
+ float base = 1.0 - clamp((f.uwp.y - f.uOrigin.y) / max(0.7 * f.plantH, 0.05), 0.0, 1.0);
181
+ vec3 q = floor(clamp(soil, 0.0, 1.0) * 127.0 + 0.5);
182
+ gtPacked = q.r * 16384.0 + q.g * 128.0 + q.b;
183
+ gtBlend = base * 0.85;
184
+ }
185
+ }
186
+ material.variation = vec4(f.hash, f.alive, f.tintN, gtPacked);
187
+ material.baked = vec4(sunShare, through, up, gtBlend);
188
+ }
189
+ }
190
+
191
+ fragment {
192
+ void material(inout MaterialInputs material) {
193
+ prepareMaterial(material);
194
+ vec4 base = texture(materialParams_baseColorMap, getUV0()) * materialParams.baseColorFactor;
195
+ // the per-instance tint and the leaves' world-noise patches (foliage.mat's comments)
196
+ float hash = variable_variation.x;
197
+ vec3 tint = mix(vec3(0.86, 0.94, 0.72), vec3(1.06, 1.02, 1.12), hash);
198
+ base.rgb *= mix(vec3(1.0), tint, materialParams.sway.z) * (1.0 - 0.18 * variable_variation.z);
199
+ // the ground's colour at the base (the vertex shader's note): the soil's mean colour in place of the card's
200
+ if (variable_baked.w > 0.0) {
201
+ highp float pk = variable_variation.w;
202
+ vec3 soil = vec3(floor(pk / 16384.0), mod(floor(pk / 128.0), 128.0), mod(pk, 128.0)) / 127.0;
203
+ base.rgb = mix(base.rgb, soil, variable_baked.w);
204
+ }
205
+ // the distance fade thins the card; the coverage holds a far crown's share over the cutoff (colour pass only:
206
+ // the shadow map's caster is fattened below already)
207
+ base.a *= variable_variation.y;
208
+ #if defined(VARIANT_DEPTH)
209
+ float cut = getMaskThreshold();
210
+ if (base.a >= 0.5 * cut) base.a = max(base.a, cut);
211
+ #else
212
+ base.a = min(base.a * (1.0 + materialParams.look.w * variable_radial.w), 1.0);
213
+ #endif
214
+ material.baseColor = base;
215
+ material.metallic = 0.0;
216
+ material.roughness = 1.0;
217
+ material.reflectance = 0.0;
218
+ material.ambientOcclusion = 0.0; // Filament's IBL off: the ambient is the vertex shader's, below
219
+ material.emissive = vec4(base.rgb * variable_lightv.rgb, 0.0);
220
+ }
221
+
222
+ // PER LIGHT (the sun first): the vertex shader's wrapped Lambert + translucency scalar, times the light and its
223
+ // SHADOW (a card takes the shadow map by leaf.y only, foliage.mat's rule). The shadow is the STRICTER of the
224
+ // shadow map's and the baked ground's share at the vertex (groundLightV): past the map's reach the bake alone,
225
+ // within it the two agree on the same crowns and the min keeps them from doubling. A lamp gets the sun's scalar
226
+ // too — the lean tier is for cards seen from a distance, where a lamp's own angle on a leaf is not read
227
+ vec3 surfaceShading(const MaterialInputs materialInputs, const ShadingData shadingData, const LightData lightData) {
228
+ // the shadow MAP is always softened by leaf.y: it cannot tell a neighbour's shadow from the crown's own card-on-card
229
+ // one, and a young pine at full strength went black (2026-10-18). The BAKED share can: a neighbour's crown is in it
230
+ // through 64 alpha-tested rays, so it lands in full near the ground and softens to leaf.y at the crowns' level
231
+ float ky = clamp(materialParams.leaf.y, 0.0, 1.0);
232
+ float kB = mix(1.0, ky, variable_baked.z);
233
+ // the baked share is the SUN's: a lamp takes its own shadow map alone (foliage.mat's test for the sun)
234
+ bool sun = dot(lightData.l, frameUniforms.lightDirection) > 0.999;
235
+ float baked = sun ? mix(1.0, variable_baked.x, kB) : 1.0;
236
+ float lit = min(mix(1.0, lightData.visibility, ky), baked); // the direct light
237
+ // the translucency takes the same two shadows at the HEIGHT weight (kB): a blade at the ground in a tree's baked shade
238
+ // gets none of the sun through it (the map alone leaked 30-40 % of the sun into every shaded clump, and all of it past
239
+ // castDistance - the owner, 2026-10-18 "трава светлая в тени"); a card at the crowns' level keeps leaf.y of it
240
+ float litT = sun ? mix(1.0, min(lightData.visibility, variable_baked.x), kB) : mix(1.0, lightData.visibility, ky);
241
+ float reach = lightData.colorIntensity.w * lightData.attenuation;
242
+ return shadingData.diffuseColor * lightData.colorIntensity.rgb * (reach * (lit * variable_lightv.w + litT * variable_baked.y) / PI);
243
+ }
244
+ }
@@ -149,9 +149,11 @@ fragment {
149
149
  float alpha = tex.a * tint.a * variable_custom.z * shape;
150
150
 
151
151
  // Soft particles (depthFade > 0, metres): fade the sprite out where it cuts into the scene
152
- // geometry behind it. sampler0_structure is Filament's depth ("structure") buffer — bound
153
- // to the colour pass by patch 0017 while creator-gl sees a drawn system with depthFade > 0.
154
- // With the structure pass OFF (setDepthEffects(false) = the LOW graphics tier, or a scene
152
+ // geometry behind it. sampler0_structure is the scene depth — the colour pass's own depth
153
+ // after the opaque objects, bound to the blended half by fork patch 0017 while creator-gl's
154
+ // scene_depth.cpp sees a system with depthFade > 0 in the scene (full resolution, the
155
+ // pixel's own texel: a NEAREST read at the fragment's clip position is exact).
156
+ // With the capture OFF (setDepthEffects(false) = the LOW graphics tier, or a scene
155
157
  // nothing asked it for) filament binds a 1x1 dummy that reads 1.0 — in reversed-Z that is
156
158
  // the NEAR plane, i.e. "the scene is in front of every sprite", so the fade would take the
157
159
  // whole system to alpha 0 and it would VANISH instead of turning hard. Real geometry never
@@ -95,9 +95,11 @@ fragment {
95
95
  float alpha = tex.a * tint.a * variable_custom.z * shape;
96
96
 
97
97
  // Soft particles (depthFade > 0, metres): fade the sprite out where it cuts into the scene
98
- // geometry behind it. sampler0_structure is Filament's depth ("structure") buffer — bound
99
- // to the colour pass by patch 0017 while creator-gl sees a drawn system with depthFade > 0.
100
- // With the structure pass OFF (setDepthEffects(false) = the LOW graphics tier, or a scene
98
+ // geometry behind it. sampler0_structure is the scene depth — the colour pass's own depth
99
+ // after the opaque objects, bound to the blended half by fork patch 0017 while creator-gl's
100
+ // scene_depth.cpp sees a system with depthFade > 0 in the scene (full resolution, the
101
+ // pixel's own texel: a NEAREST read at the fragment's clip position is exact).
102
+ // With the capture OFF (setDepthEffects(false) = the LOW graphics tier, or a scene
101
103
  // nothing asked it for) filament binds a 1x1 dummy that reads 1.0 — in reversed-Z that is
102
104
  // the NEAR plane, i.e. "the scene is in front of every sprite", so the fade would take the
103
105
  // whole system to alpha 0 and it would VANISH instead of turning hard. Real geometry never