lecodes-cli 2.3.1 → 2.4.1

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.
package/dist/index.js CHANGED
@@ -64,7 +64,7 @@ var package_default;
64
64
  var init_package = __esm(() => {
65
65
  package_default = {
66
66
  name: "lecodes-cli",
67
- version: "2.3.1",
67
+ version: "2.4.1",
68
68
  dependencies: {
69
69
  "@letary/chisel": "^0.15.0",
70
70
  jimp: "^1.6.1",
@@ -88,7 +88,7 @@ var init_package = __esm(() => {
88
88
  },
89
89
  peerDependencies: {
90
90
  "lecodes-design": "^0.11.0",
91
- "lecodes-headless": "^2.3.1",
91
+ "lecodes-headless": "^2.4.1",
92
92
  "lecodes-3d-editor": "^0.9.0",
93
93
  "lecodes-assets": "^0.2.1",
94
94
  "heic-decode": "^2.1.0",
@@ -158,7 +158,7 @@ var init_package = __esm(() => {
158
158
  });
159
159
 
160
160
  // ../../sdk/src/version.ts
161
- var SDK_VERSION = "2.3.1";
161
+ var SDK_VERSION = "2.4.1";
162
162
 
163
163
  // src/cli/args.ts
164
164
  var isNegativeNumber = (s) => /^-(\d|\.\d)/.test(s), isValue = (s) => !s.startsWith("-") || isNegativeNumber(s), parseArgv = (argv, booleans = new Set) => {
@@ -171004,6 +171004,7 @@ var extractMatBlocks = (text) => {
171004
171004
  culling: "back",
171005
171005
  depthWrite: true,
171006
171006
  shadowMultiplier: false,
171007
+ flipUV: true,
171007
171008
  params: [],
171008
171009
  samplers: [],
171009
171010
  fragmentCode: "",
@@ -171056,6 +171057,7 @@ var extractMatBlocks = (text) => {
171056
171057
  culling,
171057
171058
  depthWrite,
171058
171059
  shadowMultiplier: def.shadowMultiplier === true,
171060
+ flipUV: def.flipUV !== false,
171059
171061
  params,
171060
171062
  samplers,
171061
171063
  fragmentCode,
@@ -215184,6 +215186,9 @@ var windowEnv = (opts) => {
215184
215186
  if (opts.frameRate !== undefined && process.env.CREATOR_FRAME_RATE === undefined) {
215185
215187
  env.CREATOR_FRAME_RATE = String(opts.frameRate);
215186
215188
  }
215189
+ if (opts.vsync !== undefined && process.env.CREATOR_VSYNC === undefined) {
215190
+ env.CREATOR_VSYNC = opts.vsync ? "1" : "0";
215191
+ }
215187
215192
  if (opts.mouseEmulateTouch !== undefined && process.env.CREATOR_MOUSE_TOUCH === undefined) {
215188
215193
  env.CREATOR_MOUSE_TOUCH = opts.mouseEmulateTouch ? "1" : "0";
215189
215194
  }
@@ -215880,7 +215885,9 @@ var buildDesktop = async (flags) => {
215880
215885
  ...desktop ? { desktop: {
215881
215886
  window: desktop.window,
215882
215887
  ...desktop.mouseEmulateTouch !== undefined ? { mouseEmulateTouch: desktop.mouseEmulateTouch } : {},
215883
- ...desktop.virtualKeyboard !== undefined ? { virtualKeyboard: desktop.virtualKeyboard } : {}
215888
+ ...desktop.virtualKeyboard !== undefined ? { virtualKeyboard: desktop.virtualKeyboard } : {},
215889
+ ...desktop.frameRate !== undefined ? { frameRate: desktop.frameRate } : {},
215890
+ ...desktop.vsync !== undefined ? { vsync: desktop.vsync } : {}
215884
215891
  } } : {}
215885
215892
  });
215886
215893
  if (config.icon) {
@@ -217893,6 +217900,7 @@ var run = defineCommand({
217893
217900
  renderScale: resolveRenderScale(flags["render-scale"], appCfg?.desktop?.renderScale),
217894
217901
  maxRenderSize: appCfg?.desktop?.maxRenderSize,
217895
217902
  frameRate: appCfg?.desktop?.frameRate,
217903
+ vsync: appCfg?.desktop?.vsync,
217896
217904
  mouseEmulateTouch: appCfg?.desktop?.mouseEmulateTouch,
217897
217905
  virtualKeyboard: appCfg?.desktop?.virtualKeyboard,
217898
217906
  fps: flags.fps,
@@ -219049,6 +219057,7 @@ ${crashes.join(`
219049
219057
  title: win?.title ?? appCfg?.name ?? name,
219050
219058
  scale: resolveScale(undefined, win?.scale),
219051
219059
  frameRate: appCfg?.desktop?.frameRate,
219060
+ vsync: appCfg?.desktop?.vsync,
219052
219061
  fps: flags.fps
219053
219062
  }).then(() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."), (e) => warnErr(`Desktop host: ${e instanceof Error ? e.message : String(e)}`));
219054
219063
  }
@@ -228246,6 +228255,7 @@ init_browserAuth();
228246
228255
  init_cli();
228247
228256
 
228248
228257
  // src/web/build.ts
228258
+ init_compile();
228249
228259
  import { createHash as createHash9 } from "node:crypto";
228250
228260
  import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync53, mkdirSync as mkdirSync34, readFileSync as readFileSync42, readdirSync as readdirSync24, rmSync as rmSync17, statSync as statSync20, writeFileSync as writeFileSync36 } from "node:fs";
228251
228261
  import { dirname as dirname27, extname as extname2, join as join56, resolve as resolve35 } from "node:path";
@@ -228281,14 +228291,21 @@ var buildWeb = async (options) => {
228281
228291
  const base = normalBase(config.base);
228282
228292
  const warnings = [];
228283
228293
  const assetUrl = (path) => "assets" + encodeURI(path);
228284
- const collected = collectLocalEntries(root, manifest, "assets");
228294
+ const collected = collectLocalEntries(root, manifest, "assets", { localShaders: true });
228285
228295
  const entries = collected.entries.filter((e) => {
228286
228296
  const abs = e.absPath ? resolve35(e.absPath) : null;
228287
228297
  return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")));
228288
228298
  });
228289
- for (const w of collected.warnings)
228290
- if (!/shaders compile on the server/.test(w))
228291
- warnings.push(w);
228299
+ warnings.push(...collected.warnings);
228300
+ const shaders = new Map;
228301
+ for (const entry of entries) {
228302
+ if (entry.type !== "shader" || !entry.absPath)
228303
+ continue;
228304
+ const text = readFileSync42(entry.absPath, "utf8");
228305
+ const hash = createHash9("sha256").update(text).digest("hex").slice(0, 16);
228306
+ shaders.set(hash, JSON.stringify(buildLiteMaterialArtifact(text)));
228307
+ entry.shaders = [{ platform: "opengl", src: `shaders/${hash}_opengl.filamat` }];
228308
+ }
228292
228309
  const fontsHeader = await applyProjectFonts(entries, {
228293
228310
  mode: "vendor",
228294
228311
  root,
@@ -228374,6 +228391,12 @@ var buildWeb = async (options) => {
228374
228391
  mkdirSync34(dirname27(to), { recursive: true });
228375
228392
  copyFileSync2(entry.absPath, to);
228376
228393
  }
228394
+ for (const [hash, artifact] of shaders) {
228395
+ if (!js.includes(`shaders/${hash}_`))
228396
+ continue;
228397
+ mkdirSync34(join56(out, "shaders"), { recursive: true });
228398
+ writeFileSync36(join56(out, "shaders", `${hash}_webgl-lite.filamat`), artifact);
228399
+ }
228377
228400
  const app = readAppConfigAt(root);
228378
228401
  let html = readFileSync42(join56(lite, "index.html"), "utf8");
228379
228402
  const head = [];
package/package.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "name": "lecodes-cli",
3
- "version": "2.3.1",
3
+ "version": "2.4.1",
4
4
  "dependencies": {
5
5
  "@letary/chisel": "^0.15.0",
6
6
  "jimp": "^1.6.1",
7
- "lecodes-sdk": "2.3.1"
7
+ "lecodes-sdk": "2.4.1"
8
8
  },
9
9
  "devDependencies": {
10
10
  "lecodes-design": "https://storage.yandexcloud.net/le-codes/releases/packages/lecodes-design/lecodes-design-0.11.0.tgz",
11
- "lecodes-headless": "2.3.1",
11
+ "lecodes-headless": "2.4.1",
12
12
  "qrcode-terminal": "^0.12.0",
13
13
  "lecodes-3d-editor": "https://storage.yandexcloud.net/le-codes/releases/packages/lecodes-3d-editor/lecodes-3d-editor-0.9.0.tgz",
14
14
  "lecodes-assets": "0.2.1",
15
15
  "lecodes-creator-nav": "0.19.0",
16
- "lecodes-gl-lite": "2.3.1",
17
- "lecodes-web-frame": "2.3.1",
18
- "lecodes-web-player": "2.3.1",
19
- "lecodes-web-shared": "2.3.1",
16
+ "lecodes-gl-lite": "2.4.1",
17
+ "lecodes-web-frame": "2.4.1",
18
+ "lecodes-web-player": "2.4.1",
19
+ "lecodes-web-shared": "2.4.1",
20
20
  "lecodes-web-lite": "2.0.2",
21
21
  "heic-decode": "^2.1.0",
22
22
  "marcidb-embedded": "^0.15.0",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "peerDependencies": {
26
26
  "lecodes-design": "^0.11.0",
27
- "lecodes-headless": "^2.3.1",
27
+ "lecodes-headless": "^2.4.1",
28
28
  "lecodes-3d-editor": "^0.9.0",
29
29
  "lecodes-assets": "^0.2.1",
30
30
  "heic-decode": "^2.1.0",
@@ -0,0 +1,251 @@
1
+ // foliage-cards: the LEAN material of the vegetation tier's CARDS (leaves, needles, grass blades) — the same wind
2
+ // (foliage-wind.vs) and the same parameters as foliage.mat, with the fragment cut to what a card seen past a few
3
+ // metres needs (2026-10-18, the Forest testbed: foliage.mat's masked fragment was ~15 ms of a 49 ms frame on the
4
+ // Radeon — a third of the forest). What moved or went:
5
+ // • the LIGHTING is PER VERTEX: the sky's SH on the crown's hull normal (the ambient) and the sun's wrapped
6
+ // Lambert + back-lit translucency (one scalar), interpolated over the card. Only the sun's SHADOW stays per
7
+ // pixel — Filament's lights loop hands it to surfaceShading() with every light (the material is `lit` with
8
+ // customSurfaceShading for that alone: an unlit material gets no shadow variant)
9
+ // • ONE texture read (the base colour); the normal map, metallic / roughness, occlusion and emissive maps are
10
+ // declared (gltfio fills them) and never read; no tangent frame (`requires` has no tangents)
11
+ // • no derivatives: the shading normal is the hull's (foliage.mat blends the card's own plane in), the mip
12
+ // coverage is estimated from the DISTANCE in the vertex shader (log2 of the distance over ~8 m), there is no
13
+ // edge fade
14
+ // • THE GROUND'S BAKED LIGHT is read per vertex too (groundLightV, 2026-10-18): the ambient under the vertex and the
15
+ // sun's baked share where the vertex's own sun ray meets the ground, so trees past the shadow map's reach stand in
16
+ // their neighbours' baked shadow; without a bound terrain the scene's sky lights every card
17
+ // The engine treats it as foliage (it declares `benders`) and writes the same blocks; `Material.foliageCards`
18
+ // in the SDK picks it for a model's MASK materials in place of foliage-masked.
19
+
20
+ material {
21
+ name : foliage_cards,
22
+ shadingModel : lit,
23
+ customSurfaceShading : true,
24
+ specularAmbientOcclusion : simple,
25
+ doubleSided : true,
26
+ blending : masked,
27
+ alphaToCoverage : true,
28
+ flipUV : false,
29
+ requires : [ uv0, color ],
30
+ variables : [ variation, radial, lightv, baked ],
31
+ parameters : [
32
+ { type : float4, name : baseColorFactor },
33
+ { type : sampler2d, name : baseColorMap },
34
+ { type : float, name : metallicFactor },
35
+ { type : float, name : roughnessFactor },
36
+ { type : float3, name : emissiveFactor },
37
+ { type : float, name : normalScale },
38
+ { type : float, name : aoStrength },
39
+ { type : sampler2d, name : normalMap },
40
+ { type : float, name : hasNormalMap },
41
+ { type : sampler2d, name : metallicRoughnessMap },
42
+ { type : sampler2d, name : occlusionMap },
43
+ { type : sampler2d, name : emissiveMap },
44
+ // ---- foliage (foliage.mat has every comment) ----
45
+ { type : float4, name : wind },
46
+ { type : float4, name : sway },
47
+ { type : float4, name : fade },
48
+ { type : float4, name : leaf },
49
+ { type : float4, name : profile },
50
+ { type : float, name : shiverDir },
51
+ { type : float4, name : look },
52
+ { type : sampler2d, name : windNoise },
53
+ { type : float4, name : plant },
54
+ { type : float4[8], name : benders },
55
+ { type : float4[8], name : benderMotion },
56
+ // ---- THE GROUND'S LIGHT (foliage.mat's block, read PER VERTEX here; 2026-10-18, the Forest's baked ground) ----
57
+ // level-wide, written by creator-gl once a terrain's rect is bound (glb.cpp bindGround / bindOptions)
58
+ { type : float4, name : ground, precision : high },
59
+ { type : float4, name : groundInv, precision : high },
60
+ { type : float4, name : groundST, precision : high },
61
+ { type : sampler2d, name : lightmapLight, precision : high },
62
+ { type : sampler2d, name : lightmapAux },
63
+ { type : float, name : lightScale },
64
+ { type : float4, name : sunDir },
65
+ { type : float4, name : sunLight, precision : high },
66
+ // ---- THE GROUND'S COLOUR (2026-10-18, glb.cpp foliageSetGroundTint): the terrain's control map (its four layers'
67
+ // weights) and each layer's mean colour; a card's base takes the colour of the soil under it, its top its own
68
+ { type : sampler2d, name : groundControl },
69
+ { type : float4[4], name : groundTint },
70
+ { type : float4, name : groundTintOrigin, precision : high }, // xy = the terrain's world xz origin, z = bound
71
+ { type : float4, name : groundTintInv, precision : high } // the inverse of its world xz axes
72
+ ]
73
+ }
74
+
75
+ vertex {
76
+ #include "foliage-wind.vs"
77
+ #include "lightmap-sunmap.fs"
78
+
79
+ // THE GROUND'S LIGHT AT A VERTEX (foliage.mat reads it per pixel, under the pixel): true when the plant stands over
80
+ // the bound terrain. `ambient` = the terrain texel's irradiance under the vertex without the sun's part (lux, on the
81
+ // up normal) — the sky through the canopy, the bounce. `sunShare` = the baked share of the sun's disc at the point
82
+ // where THIS VERTEX'S OWN SUN RAY meets the ground (the plant's base height): what shades that point shades the
83
+ // vertex too — a neighbour's crown, or the crown above a card deep in it — so a tree past the shadow map's reach
84
+ // still stands in its neighbour's shadow, and the grass under a pine in the pine's. `lod` picks the pages' level
85
+ // (a far vertex over one texel of 25 cm would flicker between texels as it sways).
86
+ // the crowns' level over the ground: a vertex this high takes the open sky's ambient and its own crown's softened shadow
87
+ #define CANOPY 12.0
88
+
89
+ bool groundLightV(highp vec3 uwp, highp float baseY, float lod, out highp vec3 ambient, out float sunShare) {
90
+ ambient = vec3(0.0); sunShare = 1.0;
91
+ if (materialParams.ground.z < 0.5) return false;
92
+ highp vec2 d = uwp.xz - materialParams.ground.xy;
93
+ highp vec2 g = vec2(dot(materialParams.groundInv.xy, d), dot(materialParams.groundInv.zw, d));
94
+ if (g.x < 0.0 || g.y < 0.0 || g.x > 1.0 || g.y > 1.0) return false; // off the terrain
95
+ highp vec2 luv = g * materialParams.groundST.xy + materialParams.groundST.zw;
96
+ highp vec3 E = textureLod(materialParams_lightmapLight, luv, lod).rgb * materialParams.lightScale;
97
+ ambient = E;
98
+ if (materialParams.sunDir.w > 0.5) {
99
+ highp ivec2 auxSize = textureSize(materialParams_lightmapAux, 0);
100
+ highp int level = clamp(int(lod + 0.5), 0, max(sunMapLevels(), 1) - 1);
101
+ // the sun's part out of the ground's E under the vertex: its illuminance x the share there x the cosine on the
102
+ // ground, taken as flat
103
+ float shareHere = sunMapAt(luv, auxSize, level);
104
+ ambient = max(E - materialParams.sunLight.rgb * (shareHere * max(materialParams.sunDir.y, 0.0)), vec3(0.0));
105
+ // the sun ray through the vertex, followed down to the plant's base height
106
+ vec3 s = materialParams.sunDir.xyz; // TO the sun
107
+ highp vec3 foot = uwp - s * ((uwp.y - baseY) / max(s.y, 0.05));
108
+ d = foot.xz - materialParams.ground.xy;
109
+ g = clamp(vec2(dot(materialParams.groundInv.xy, d), dot(materialParams.groundInv.zw, d)), 0.0, 1.0);
110
+ sunShare = sunMapAt(g * materialParams.groundST.xy + materialParams.groundST.zw, auxSize, level);
111
+ }
112
+ return true;
113
+ }
114
+
115
+ vec3 shIrradianceV(const vec3 n) {
116
+ return max(
117
+ frameUniforms.iblSH[0].xyz
118
+ + frameUniforms.iblSH[1].xyz * n.y + frameUniforms.iblSH[2].xyz * n.z + frameUniforms.iblSH[3].xyz * n.x
119
+ + frameUniforms.iblSH[4].xyz * (n.y * n.x) + frameUniforms.iblSH[5].xyz * (n.y * n.z)
120
+ + frameUniforms.iblSH[6].xyz * (3.0 * n.z * n.z - 1.0) + frameUniforms.iblSH[7].xyz * (n.z * n.x)
121
+ + frameUniforms.iblSH[8].xyz * (n.x * n.x - n.y * n.y), 0.0);
122
+ }
123
+
124
+ void materialVertex(inout MaterialVertexInputs material) {
125
+ FoliageVertex f = foliageWind(material);
126
+ // COVERAGE from the distance (foliage.mat reads the atlas' mip level from the uv derivatives per pixel): the
127
+ // atlas texels under a pixel double every doubling of the distance past ~8 m, where a card is still read in
128
+ // full; `look.w` scales how much of that goes into the alpha
129
+ float lodEst = max(log2(max(f.dist, 1.0) / 8.0), 0.0);
130
+ material.radial = vec4(f.hull, lodEst);
131
+ // THE LIGHT, per vertex. Ambient = the sky's SH on the hull normal, as radiance under the camera's exposure
132
+ // (frameUniforms.iblLuminance holds it), darkened by the baked sky visibility by `leaf.w`. The sun = a wrapped
133
+ // Lambert on the hull (thin leaves let light past the terminator, `sway.w`) plus the translucency of a card lit
134
+ // from behind (`leaf.z`, narrowed toward the view line into the light; less deep in the crown), one scalar the
135
+ // fragment multiplies by the light's colour, intensity and SHADOW
136
+ float ao = mix(1.0, f.vao, clamp(materialParams.leaf.w, 0.0, 1.0));
137
+ vec3 skyAmbient = shIrradianceV(f.hull) * frameUniforms.iblLuminance;
138
+ vec3 ambient = skyAmbient;
139
+ float sunShare = 1.0;
140
+ float up = 1.0; // the vertex's height over the ground as a share of the canopy's (1 without a ground)
141
+ highp vec3 gAmb;
142
+ if (groundLightV(f.uwp, f.uOrigin.y, lodEst, gAmb, sunShare)) {
143
+ // THE BAKED GROUND'S LIGHT (the function above): its ambient on the hull normal by the hemisphere formula
144
+ // lightmapShade uses for a tilted normal (the light comes from above), as radiance E / pi under the camera's
145
+ // exposure. The crown's base takes the ground's ambient (the sky through the canopy), its top the open sky's
146
+ // SH - blended by the vertex's height over the plant; a grass blade is all ground
147
+ // the hemisphere formula alone gave a down-facing hull nothing (black needles under a young pine, the owner
148
+ // 2026-10-18): what a card sees below is the ground, lit by that same E and reflecting a quarter of it
149
+ vec3 groundAmbient = gAmb * mix(0.25, 1.0, (1.0 + f.hull.y) * 0.5) / PI * frameUniforms.exposure;
150
+ // by the height OVER THE GROUND, not within the plant: a bush's top is under the same crowns as the ground
151
+ // 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);
152
+ // the open sky is reached at the crowns' level, CANOPY metres up
153
+ up = clamp((f.uwp.y - f.uOrigin.y) / CANOPY, 0.0, 1.0);
154
+ ambient = mix(groundAmbient, skyAmbient, up);
155
+ }
156
+ ambient *= ao;
157
+ vec3 l = frameUniforms.lightDirection;
158
+ vec3 v = normalize(getWorldCameraPosition() - f.wp);
159
+ float wrap = clamp(materialParams.sway.w, 0.0, 1.0);
160
+ float NoL = max((dot(f.hull, l) + wrap) / (1.0 + wrap), 0.0);
161
+ float back = max(-dot(f.hull, l), 0.0);
162
+ float vl = pow(max(dot(-v, l), 0.0), 4.0);
163
+ float through = clamp(materialParams.leaf.z, 0.0, 1.0) * back * (0.5 + 0.5 * vl) * mix(1.0, f.vao, 0.5);
164
+ material.lightv = vec4(ambient, NoL);
165
+ // .x = the baked share of the sun at this vertex (1 without a bound ground): surfaceShading takes it as a second
166
+ // shadow map on the direct light, the stricter of the two, never the product. .y = the translucency, which takes
167
+ // the shadow map ALONE: the bake's 64-ray share through the crown put out the glow of a back-lit crown (the
168
+ // multiple scattering a real crown has and this model lacks; the owner, 2026-10-18 "лес довольно тёмный")
169
+ // .z = the height share: near the ground a shadow is a NEIGHBOUR's and lands in full, at the crowns' level it is the
170
+ // crown's own and is softened by leaf.y (the scattering a real crown has); the fragment weighs the shadow by it.
171
+ // (The plants' SHADOW LOD is the LOD pass's, per renderable - lod.cpp gFoliageCast - not this shader's: a card-level
172
+ // discard in the depth variant saved 0.6 ms of the pass's 6.8, the renderables' vertices and draws are the cost)
173
+ // THE GROUND'S COLOUR at the plant's base: the layers' weights under the PLANT (one colour per plant - a varying
174
+ // that differs per vertex could not carry a packed value) times their mean colours; the fragment blends the card's
175
+ // colour toward it by `baked.w` - all of it at the ground, none from ~70 % of the plant's height up (a grass
176
+ // blade's base is its soil, a bush's crown its own leaves; a tree's cards never reach it). The colour rides
177
+ // `variation.w` packed 7 bits a channel (matc allows four variables with the colour attribute; vao was unused there)
178
+ float gtPacked = 0.0, gtBlend = 0.0;
179
+ if (materialParams.groundTintOrigin.z > 0.5) {
180
+ highp vec2 td = f.uOrigin.xz - materialParams.groundTintOrigin.xy;
181
+ highp vec2 tg = vec2(dot(materialParams.groundTintInv.xy, td), dot(materialParams.groundTintInv.zw, td));
182
+ if (tg.x >= 0.0 && tg.y >= 0.0 && tg.x <= 1.0 && tg.y <= 1.0) {
183
+ vec4 w = textureLod(materialParams_groundControl, tg, 0.0);
184
+ float ws = max(w.x + w.y + w.z + w.w, 1e-3);
185
+ vec3 soil = (materialParams.groundTint[0].rgb * w.x + materialParams.groundTint[1].rgb * w.y
186
+ + materialParams.groundTint[2].rgb * w.z + materialParams.groundTint[3].rgb * w.w) / ws;
187
+ float base = 1.0 - clamp((f.uwp.y - f.uOrigin.y) / max(0.7 * f.plantH, 0.05), 0.0, 1.0);
188
+ vec3 q = floor(clamp(soil, 0.0, 1.0) * 127.0 + 0.5);
189
+ gtPacked = q.r * 16384.0 + q.g * 128.0 + q.b;
190
+ gtBlend = base * 0.85;
191
+ }
192
+ }
193
+ material.variation = vec4(f.hash, f.alive, f.tintN, gtPacked);
194
+ material.baked = vec4(sunShare, through, up, gtBlend);
195
+ }
196
+ }
197
+
198
+ fragment {
199
+ void material(inout MaterialInputs material) {
200
+ prepareMaterial(material);
201
+ vec4 base = texture(materialParams_baseColorMap, getUV0()) * materialParams.baseColorFactor;
202
+ // the per-instance tint and the leaves' world-noise patches (foliage.mat's comments)
203
+ float hash = variable_variation.x;
204
+ vec3 tint = mix(vec3(0.86, 0.94, 0.72), vec3(1.06, 1.02, 1.12), hash);
205
+ base.rgb *= mix(vec3(1.0), tint, materialParams.sway.z) * (1.0 - 0.18 * variable_variation.z);
206
+ // the ground's colour at the base (the vertex shader's note): the soil's mean colour in place of the card's
207
+ if (variable_baked.w > 0.0) {
208
+ highp float pk = variable_variation.w;
209
+ vec3 soil = vec3(floor(pk / 16384.0), mod(floor(pk / 128.0), 128.0), mod(pk, 128.0)) / 127.0;
210
+ base.rgb = mix(base.rgb, soil, variable_baked.w);
211
+ }
212
+ // the distance fade thins the card; the coverage holds a far crown's share over the cutoff (colour pass only:
213
+ // the shadow map's caster is fattened below already)
214
+ base.a *= variable_variation.y;
215
+ #if defined(VARIANT_DEPTH)
216
+ float cut = getMaskThreshold();
217
+ if (base.a >= 0.5 * cut) base.a = max(base.a, cut);
218
+ #else
219
+ base.a = min(base.a * (1.0 + materialParams.look.w * variable_radial.w), 1.0);
220
+ #endif
221
+ material.baseColor = base;
222
+ material.metallic = 0.0;
223
+ material.roughness = 1.0;
224
+ material.reflectance = 0.0;
225
+ material.ambientOcclusion = 0.0; // Filament's IBL off: the ambient is the vertex shader's, below
226
+ material.emissive = vec4(base.rgb * variable_lightv.rgb, 0.0);
227
+ }
228
+
229
+ // PER LIGHT (the sun first): the vertex shader's wrapped Lambert + translucency scalar, times the light and its
230
+ // SHADOW (a card takes the shadow map by leaf.y only, foliage.mat's rule). The shadow is the STRICTER of the
231
+ // shadow map's and the baked ground's share at the vertex (groundLightV): past the map's reach the bake alone,
232
+ // within it the two agree on the same crowns and the min keeps them from doubling. A lamp gets the sun's scalar
233
+ // too — the lean tier is for cards seen from a distance, where a lamp's own angle on a leaf is not read
234
+ vec3 surfaceShading(const MaterialInputs materialInputs, const ShadingData shadingData, const LightData lightData) {
235
+ // the shadow MAP is always softened by leaf.y: it cannot tell a neighbour's shadow from the crown's own card-on-card
236
+ // one, and a young pine at full strength went black (2026-10-18). The BAKED share can: a neighbour's crown is in it
237
+ // through 64 alpha-tested rays, so it lands in full near the ground and softens to leaf.y at the crowns' level
238
+ float ky = clamp(materialParams.leaf.y, 0.0, 1.0);
239
+ float kB = mix(1.0, ky, variable_baked.z);
240
+ // the baked share is the SUN's: a lamp takes its own shadow map alone (foliage.mat's test for the sun)
241
+ bool sun = dot(lightData.l, frameUniforms.lightDirection) > 0.999;
242
+ float baked = sun ? mix(1.0, variable_baked.x, kB) : 1.0;
243
+ float lit = min(mix(1.0, lightData.visibility, ky), baked); // the direct light
244
+ // the translucency takes the same two shadows at the HEIGHT weight (kB): a blade at the ground in a tree's baked shade
245
+ // 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
246
+ // castDistance - the owner, 2026-10-18 "трава светлая в тени"); a card at the crowns' level keeps leaf.y of it
247
+ float litT = sun ? mix(1.0, min(lightData.visibility, variable_baked.x), kB) : mix(1.0, lightData.visibility, ky);
248
+ float reach = lightData.colorIntensity.w * lightData.attenuation;
249
+ return shadingData.diffuseColor * lightData.colorIntensity.rgb * (reach * (lit * variable_lightv.w + litT * variable_baked.y) / PI);
250
+ }
251
+ }