lecodes-cli 2.5.0 → 2.6.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.
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "lecodes-cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "dependencies": {
5
5
  "@letary/chisel": "^0.15.0",
6
6
  "jimp": "^1.6.1",
7
- "lecodes-sdk": "2.5.0",
7
+ "lecodes-sdk": "2.6.0",
8
8
  "selfsigned": "^2.4.1"
9
9
  },
10
10
  "devDependencies": {
11
11
  "lecodes-design": "https://storage.yandexcloud.net/le-codes/releases/packages/lecodes-design/lecodes-design-0.11.0.tgz",
12
- "lecodes-headless": "2.5.0",
12
+ "lecodes-headless": "2.6.0",
13
13
  "qrcode-terminal": "^0.12.0",
14
14
  "lecodes-3d-editor": "https://storage.yandexcloud.net/le-codes/releases/packages/lecodes-3d-editor/lecodes-3d-editor-0.9.0.tgz",
15
- "lecodes-assets": "2.5.0",
15
+ "lecodes-assets": "2.6.0",
16
16
  "lecodes-creator-nav": "0.19.0",
17
- "lecodes-gl-lite": "2.5.0",
18
- "lecodes-web-frame": "2.5.0",
19
- "lecodes-web-player": "2.5.0",
20
- "lecodes-web-shared": "2.5.0",
17
+ "lecodes-gl-lite": "2.6.0",
18
+ "lecodes-web-frame": "2.6.0",
19
+ "lecodes-web-player": "2.6.0",
20
+ "lecodes-web-shared": "2.6.0",
21
21
  "lecodes-web-lite": "2.0.7",
22
22
  "heic-decode": "^2.1.0",
23
23
  "marcidb-embedded": "^0.15.0",
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "peerDependencies": {
27
27
  "lecodes-design": "^0.11.0",
28
- "lecodes-headless": "^2.5.0",
28
+ "lecodes-headless": "^2.6.0",
29
29
  "lecodes-3d-editor": "^0.9.0",
30
- "lecodes-assets": "^2.5.0",
30
+ "lecodes-assets": "^2.6.0",
31
31
  "heic-decode": "^2.1.0",
32
32
  "marcidb-embedded": "^0.15.0",
33
33
  "sharp": "^0.35.2",
@@ -55,7 +55,8 @@ material {
55
55
  { type : sampler2d, name : windNoise },
56
56
  { type : float4, name : plant },
57
57
  { type : float4[8], name : benders },
58
- { type : float4[8], name : benderMotion }
58
+ { type : float4[8], name : benderMotion },
59
+ { type : float, name : debugMode } // the impostor lab's DEBUG VIEW (glb.cpp CREATOR_FOLIAGE_DEBUG; 0 = off): 1 albedo, 2 hull normal as colour, 3 the sky term on white — the sun off in all three
59
60
  ]
60
61
  }
61
62
 
@@ -85,7 +86,10 @@ vertex {
85
86
  // (`leaf.z`, narrowed toward the view line into the light; less deep in the crown), one scalar the fragment
86
87
  // multiplies by the light's colour, intensity and SHADOW
87
88
  float ao = mix(1.0, f.vao, clamp(materialParams.leaf.w, 0.0, 1.0));
88
- vec3 ambient = shIrradianceV(f.hull) * frameUniforms.iblLuminance * ao;
89
+ // + THE SKY THROUGH THE LEAF (look.x, Foliage `skyThrough`): a card below the crown's middle faces down and would
90
+ // read the environment's ground alone - a tenth of the zenith under an overcast sky, the lower half of every plant
91
+ // black (2026-11-04) - so the thin leaf adds that share of the sky on its other side
92
+ vec3 ambient = (shIrradianceV(f.hull) + materialParams.look.x * shIrradianceV(-f.hull)) * frameUniforms.iblLuminance * ao;
89
93
  vec3 l = frameUniforms.lightDirection;
90
94
  vec3 v = normalize(getWorldCameraPosition() - f.wp);
91
95
  float wrap = clamp(materialParams.sway.w, 0.0, 1.0);
@@ -125,6 +129,15 @@ fragment {
125
129
  material.reflectance = 0.0;
126
130
  material.ambientOcclusion = 0.0; // Filament's IBL off: the ambient is the vertex shader's, below
127
131
  material.emissive = vec4(base.rgb * variable_lightv.rgb, 0.0);
132
+ // THE DEBUG VIEW (debugMode, the impostor lab): the alpha path above stays, only the colour changes; the sun is
133
+ // off (surfaceShading returns 0). 1 = the albedo, 2 = the hull normal (world) ×½+½, 3 = the sky term on white
134
+ float dbg = materialParams.debugMode;
135
+ if (dbg > 0.5) {
136
+ vec3 hn = variable_radial.xyz;
137
+ float hl = length(hn);
138
+ hn = hl > 1e-5 ? hn / hl : vec3(0.0, 1.0, 0.0);
139
+ material.emissive = vec4(dbg < 1.5 ? base.rgb : dbg < 2.5 ? hn * 0.5 + 0.5 : variable_lightv.rgb, 0.0);
140
+ }
128
141
  }
129
142
 
130
143
  // PER LIGHT (the sun first): the vertex shader's wrapped Lambert + translucency scalar, times the light and its
@@ -132,6 +145,7 @@ fragment {
132
145
  // from the crown's own card-on-card one, and a young pine at full strength went black (2026-10-18). A lamp gets the
133
146
  // sun's scalar too — the lean tier is for cards seen from a distance, where a lamp's own angle on a leaf is not read
134
147
  vec3 surfaceShading(const MaterialInputs materialInputs, const ShadingData shadingData, const LightData lightData) {
148
+ if (materialParams.debugMode > 0.5) return vec3(0.0); // the debug view: no sun
135
149
  float lit = mix(1.0, lightData.visibility, clamp(materialParams.leaf.y, 0.0, 1.0));
136
150
  float reach = lightData.colorIntensity.w * lightData.attenuation * lit;
137
151
  return shadingData.diffuseColor * lightData.colorIntensity.rgb * (reach * (variable_lightv.w + variable_leafv.x) / PI);
@@ -24,14 +24,22 @@
24
24
  // A point p seen in a frame at a = ½ + ½·dot(p - c, right)/r, b = ½ + ½·dot(p - c, up)/r (a right, b UP) lands at
25
25
  // the atlas uv ((i + a)/A, (k + 1 - b)/E) on the grid, ((i + a)/N, (j + 1 - b)/N) on the octahedron — image row 0
26
26
  // at the top, the glTF convention (flipUV false, getUV0() raw).
27
- // • baseColorMap: rgb = albedo (sRGB), a = coverage. metallicRoughnessMap (linear): rg = the crown's HULL normal
28
- // (what the cards shade by: from 0.6 of the plant's height over its origin to the surface, foliage-wind.vs f.hull)
29
- // in OBJECT space, packed with the FULL-sphere octahedral map ×½+½ (normalDecode below, the baker's normalEncode —
27
+ // • baseColorMap: rgb = albedo (sRGB), a = COVERAGE, fractional: the baker takes S×S samples a texel (default 8×8) and
28
+ // stores covered / S². metallicRoughnessMap (linear): rg = the SHADING normal in OBJECT space — a card sample's the
29
+ // crown's HULL normal (what the cards shade by: from 0.6 of the plant's height over its origin to the surface,
30
+ // foliage-wind.vs f.hull), a bark sample's the MESH normal (the interpolated vertex NORMAL turned toward the frame's
31
+ // viewer: what foliage.mat lights the bark by), averaged over the texel's samples —
32
+ // packed with the FULL-sphere octahedral map ×½+½ (normalDecode below, the baker's normalEncode —
30
33
  // not the frame layout's hemi impEncode/impDecode; its seam pole at the corners is object −Y): a far-side leaf keeps a normal that looks away, which is the
31
34
  // crown's shading; b = the DEPTH along the frame's view axis, z = dot(p - c, d)/r stored z·½+½ (1 = the point
32
35
  // nearest this frame's viewer, ½ = the quad plane, 0 = the farthest); a = the baked sky visibility (vertexAo.ts'
33
- // COLOR_0.r). An uncovered texel carries its nearest covered neighbour's data (the dilation): a depth read just
34
- // outside a silhouette continues the surface, while the coverage there stays under the cutoff.
36
+ // COLOR_0.r). occlusionMap (linear, NOT an occlusion): r = the BARK fraction of the texel's covered samples (an
37
+ // opaque glTF material is bark, a MASK one a card), g = 0, b = the BARK's OCCLUSION: what foliage.mat darkens the bark's
38
+ // ambient by beyond the sky visibility (its baked cavity x its occlusion map x its normal map's cavity), 1/255 at least
39
+ // - b = 0 is an atlas from before the channel and reads 1. g = 0 marks the map present: the engine's default for
40
+ // an unbound sampler is a 1×1 white (g = 1), so the shader reads bark = r·(1 - g) and an older atlas reads none.
41
+ // An uncovered texel carries its nearest covered neighbour's data, albedo and bark (the dilation): a depth read just
42
+ // outside a silhouette continues the surface, while the coverage there stays 0.
35
43
  //
36
44
  // THE QUAD: 4 vertices whose POSITION is ignored (the baker writes the sphere's box corners there only so gltfio's
37
45
  // culling box is right); TEXCOORD_0 is the corner id, corner = uv·2-1 (x right, y up). The vertex shader takes the
@@ -43,11 +51,20 @@
43
51
  // (i+1,k+1) (i,k+1) (i+1,k) with (t.x+t.y-1, 1-t.x, 1-t.y), so the picture morphs between neighbouring views instead of
44
52
  // popping. On the grid i+1 wraps mod A and k+1 stops at the top row (min(k+1, E-1)); on the octahedron both stop at N-1.
45
53
  //
54
+ // THE WIND (2026-11-04): the quad sways with its tree. Each corner's world position is bent by the cards' own plant-level
55
+ // lean (foliage-wind.vs foliageField + foliageBend, included under FOLIAGE_BEND_ONLY): the wind field and the gust at the
56
+ // plant's root, the rotation about the root across the wind growing with the corner's distance from it, by the same
57
+ // `wind` / `sway` / `profile` the engine writes to every foliage material. The leaves' shiver and the benders' touch stay
58
+ // out (below a far tree's pixel; the touch is grass-scale). Only the drawn position moves: p, the frame pick and the
59
+ // lighting data describe the still tree. The depth variants run the same vertex code, so the shadow leans too. The A/B is
60
+ // `Foliage.configure({ wind: false })` (a zero field: no axis to bend about).
61
+ //
46
62
  // THE VARYINGS (Filament caps custom variables at 5; four used):
47
63
  // impFrames = (frame0, frame1, frame2, hash) — a frame packed as i·E + k (grid) / i·N + j (octahedron); hash = the
48
64
  // cards' per-instance tint hash
49
65
  // impPos = (p.xyz, w0) — p = the fragment's offset from c in object space (linear over the quad)
50
- // impW = (w1, alive, 0, 0) — w2 = 1 - w0 - w1; alive = the distance fade (`fade`, as the cards)
66
+ // impW = (w1, alive, lodEst, 0) — w2 = 1 - w0 - w1; alive = the distance fade (`fade`, as the cards);
67
+ // lodEst = the coverage boost's mip term, max(mip - 1, 0) (THE FRAGMENT)
51
68
  // impRot = the model's rotation as a quaternion (its columns normalised): getWorldFromModelMatrix() exists in the
52
69
  // vertex stage only, and the fragment needs it to take the atlas' object-space normal to the world
53
70
  // The frame choice and the weights depend on the CENTRE's direction alone, so they are the same at the four corners:
@@ -55,12 +72,28 @@
55
72
  //
56
73
  // THE FRAGMENT: per frame, unpack its index (grid: i = floor(packed/E), k = packed - i·E; octahedron: by N), decode its
57
74
  // direction (the grid's d(az_i, el_k) / the octahedron's decode), build its basis, project p onto it (no parallax — see the note at the
58
- // sample) and read both atlases; blend by the weights. Alpha = Σ w·a (× the distance fade): the glTF cutoff's mask
59
- // threshold cuts it, alphaToCoverage softens it as on the cards. The normal: normalDecode(data.rg) per frame — already
75
+ // sample) and read the three maps; blend by the weights. Alpha = Σ w·a (× the distance fade), the FRACTIONAL coverage of
76
+ // a porous crown: the glTF's mask threshold cuts it like a card's, and under MSAA alphaToCoverage turns the fraction into
77
+ // samples. Before the cut, the colour pass takes a COVERAGE BOOST: alpha × (1 + look.w × max(mip − 1, 0)), capped at 1,
78
+ // look.w = the same Foliage `coverage` the cards use. mip = log2(frameTexels / quadPixels), the atlas mip the quad
79
+ // actually reads: frameTexels = a frame's height in texels (the atlas' height / its frame rows), quadPixels = the quad's
80
+ // projected height (2r × P[1][1] / the eye's distance to the sphere's centre × the viewport's height / 2). Not the cards'
81
+ // rule (log2(dist / 8 m): a proxy for THEIR texture mip, whose blur and cut lose a card's coverage) — the atlas stores the
82
+ // TRUE coverage, exact until a frame texel shrinks under a screen pixel and its mips average a sparse crown down past
83
+ // the cut, so the boost starts only at the atlas' second mip. Measured 2026-11-04: the cards' rule made a far quad
84
+ // denser than the mesh (both / mesh only / quad only 57 / 4 / 39 against 56 / 26 / 18 without). coverage 0 = the A/B.
85
+ // The depth variants keep their own rule, no boost, as the cards'. (A dither of the fraction was built and rejected, 2026-11-04: the mesh over-covers its thin needles under its
86
+ // hard mask - mip alpha, the cards' distance boost - and against it the honest coverage read sparse, a ghost tree; the
87
+ // cut matched the mesh's coverage.) The depth variants (the shadow map, the prepass) keep the cards' hard rule — a
88
+ // texel past half the cutoff casts —, so the shadow stays solid. The normal: normalDecode(data.rg) per frame — already
60
89
  // in object space, no basis —, blended, normalised, turned to the world by the quaternion. THE LIGHT is
61
90
  // foliage-cards.mat's, per pixel: the ambient = the sky's SH on that normal × the exposure × the baked AO (by leaf.w) as
62
91
  // emissive (Filament's IBL off), the sun = a wrapped Lambert (sway.w) + the back-lit translucency (leaf.z) in one scalar
63
- // (gSun) that surfaceShading() multiplies by the light, its attenuation and its SHADOW (by leaf.y). The per-pixel normal
92
+ // (gSun) that surfaceShading() multiplies by the light, its attenuation and its SHADOW (by leaf.y). THE BARK (b = the bark
93
+ // map blended like the data): the sun = mix(that leaf scalar, max(n·l, 0) — no wrap, no translucency —, b), the shadow
94
+ // share of the map and of the self-shadow mix(leaf.y, 1, b): bark takes a shadow in full, a card its leaf.y share (as
95
+ // foliage.mat against foliage-cards.mat); the ambient stays SH(n) × AO, n already the bark's own normal there. The
96
+ // per-copy tint covers both (foliage.mat tints its bark too). The per-pixel normal
64
97
  // also becomes Filament's shading normal, the one its lights loop tests (dot(n, l) > 0) before it reads the shadow map.
65
98
  //
66
99
  // THE SELF-SHADOW: a flat quad never shades itself through the shadow map, and at these distances the mesh's own
@@ -81,8 +114,8 @@
81
114
  // multiplied by 1 - occl. The term is 1 with the sun under the horizon; `impostorSelfShadow` mixes it in (1 = on;
82
115
  // glb.cpp writes the 1, CREATOR_IMPOSTOR_SELFSHADOW=0 a 0). The depth variants (the shadow map, the prepass) skip it.
83
116
  //
84
- // The engine treats it as foliage (it declares `benders`) and writes the whole foliage block; the wind is not applied
85
- // (a far tree's sway is below a pixel). `Material.foliageImpostor` in the SDK; Model.load({ foliage: 'tree' }) arms it
117
+ // The engine treats it as foliage (it declares `benders`) and writes the whole foliage block; of the wind only the
118
+ // plant-level bend applies (THE WIND above). `Material.foliageImpostor` in the SDK; Model.load({ foliage: 'tree' }) arms it
86
119
  // with the bark and the cards, and the GLB provider picks it for a material whose extras carry `lecodes.impostor`
87
120
  // ([A, cx, cy, cz, r, E] → impostorGrid = (A, E) / impostor; E = 0 or missing = the octahedron, N = the first number).
88
121
 
@@ -131,11 +164,16 @@ material {
131
164
  // ---- the impostor (the GLB material's extras `lecodes.impostor` = [A, cx, cy, cz, r, E]) ----
132
165
  { type : float4, name : impostor, precision : high }, // (c.xyz, r): the level-0 bounding sphere, object space
133
166
  { type : float2, name : impostorGrid, precision : high }, // (A, E): A azimuths × E rows; (N, 0): N×N octahedron
134
- { type : float, name : impostorSelfShadow } // the self-shadow from the atlas: 1 on, 0 off (glb.cpp writes 1)
167
+ { type : float, name : impostorSelfShadow }, // the self-shadow from the atlas: 1 on, 0 off (glb.cpp writes 1)
168
+ { type : float, name : debugMode } // the impostor lab's DEBUG VIEW (glb.cpp CREATOR_FOLIAGE_DEBUG; 0 = off): 1 albedo, 2 hull normal as colour, 3 the sky term on white — the sun off in all three
135
169
  ]
136
170
  }
137
171
 
138
172
  vertex {
173
+ // the wind's plant-level bend (foliageField / foliageBend) without the cards' per-vertex half (a quad has no COLOR_0)
174
+ #define FOLIAGE_BEND_ONLY
175
+ #include "foliage-wind.vs"
176
+
139
177
  // the hemi-octahedral map, [0,1]² <-> a unit direction with y ≥ 0 (the baker's TS is the same formula)
140
178
  highp vec3 impDecode(highp vec2 uv) {
141
179
  highp float u = uv.x * 2.0 - 1.0;
@@ -198,6 +236,11 @@ vertex {
198
236
  highp vec3 up = cross(d, right);
199
237
  highp vec3 p = (corner.x * right + corner.y * up) * r;
200
238
  material.worldPosition.xyz = (model * vec4(c + p, 1.0)).xyz;
239
+ // THE WIND: the corner leans by the cards' own plant-level bend at its place (foliage-wind.vs foliageBend: the
240
+ // field at the root, the rotation growing with the distance from it) - the drawn quad only; p, the frame pick and
241
+ // the lighting data keep the still tree
242
+ highp vec3 uOrigin = (getUserWorldFromWorldMatrix() * vec4(model[3].xyz, 1.0)).xyz;
243
+ material.worldPosition.xyz = foliageBend(material.worldPosition.xyz, model[3].xyz, foliageField(uOrigin, getUserTime().x));
201
244
  #if defined(HAS_ATTRIBUTE_TANGENTS)
202
245
  // the quad's geometric normal faces the eye (the shadow lookup's normal bias reads it; the mesh's NORMAL is a dummy)
203
246
  highp float dwl = length(dW);
@@ -238,7 +281,6 @@ vertex {
238
281
  }
239
282
 
240
283
  // the cards' per-instance tint hash, bit for bit (foliage-wind.vs: the USER world origin of the instance)
241
- highp vec3 uOrigin = (getUserWorldFromWorldMatrix() * vec4(model[3].xyz, 1.0)).xyz;
242
284
  float hash = fract(sin(dot(uOrigin.xz, vec2(12.9898, 78.233))) * 43758.5453);
243
285
  // the distance fade (foliage-wind.vs `alive`, measured to the sphere's centre)
244
286
  float fadeEnd = materialParams.fade.y;
@@ -247,15 +289,23 @@ vertex {
247
289
  // packed i·E + k (grid) / i·N + j (octahedron: E = N)
248
290
  material.impFrames = vec4(f0.x * E + f0.y, f1.x * E + f1.y, f2.x * E + f2.y, hash);
249
291
  material.impPos = vec4(p, w.x);
250
- material.impW = vec4(w.y, alive, 0.0, 0.0);
292
+ // the coverage boost's term: the atlas mip the quad reads, past the first (the header) — a frame's texels against
293
+ // the quad's projected height (2r in the world, the model's scale included, at the eye's distance to the centre)
294
+ highp float frameTexels = float(textureSize(materialParams_baseColorMap, 0).y) / E;
295
+ highp float rW = r * length(model[1].xyz);
296
+ highp float quadPixels = 2.0 * rW * getClipFromViewMatrix()[1][1] / max(length(dW), 1e-3) * frameUniforms.resolution.y * 0.5;
297
+ float lodEst = max(log2(frameTexels / max(quadPixels, 1e-3)) - 1.0, 0.0);
298
+ material.impW = vec4(w.y, alive, lodEst, 0.0);
251
299
  mat3 R = mat3(normalize(model[0].xyz), normalize(model[1].xyz), normalize(model[2].xyz));
252
300
  material.impRot = impQuat(R);
253
301
  }
254
302
  }
255
303
 
256
304
  fragment {
257
- // from material() for surfaceShading(): the sun's scalar at this pixel — the wrapped Lambert + the translucency
305
+ // from material() for surfaceShading(): the sun's scalar at this pixel — the wrapped Lambert + the translucency (the
306
+ // bark's plain Lambert by its share) — and the bark fraction (the shadow map's share: mix(leaf.y, 1, gBark))
258
307
  float gSun = 0.0;
308
+ float gBark = 0.0;
259
309
 
260
310
  highp vec3 impDecode(highp vec2 uv) {
261
311
  highp float u = uv.x * 2.0 - 1.0;
@@ -322,15 +372,26 @@ fragment {
322
372
  // PARALLAX (the next step, not built): the data map's depth (b) exists now — the self-shadow reads it; offsetting p
323
373
  // along the view by it before the projection would line the three frames up and cut the ghosting between them.
324
374
  void impFrame(highp float packed, bool isGrid, highp vec2 cells, highp float r, highp vec3 p, highp vec2 inset,
325
- out vec4 base, out vec4 data, out highp vec3 dk, out highp vec3 rk, out highp vec3 uk) {
375
+ out vec4 base, out vec4 data, out highp vec2 uv, out highp vec3 dk, out highp vec3 rk, out highp vec3 uk) {
326
376
  highp float fi = floor(packed / cells.y + 0.5 / cells.y);
327
377
  highp float fk = packed - fi * cells.y;
328
378
  impBasis(fi, fk, isGrid, cells, dk, rk, uk);
329
- highp vec2 uv = impUv(fi, fk, cells, dot(p, rk) / r, dot(p, uk) / r, inset);
379
+ uv = impUv(fi, fk, cells, dot(p, rk) / r, dot(p, uk) / r, inset);
330
380
  base = texture(materialParams_baseColorMap, uv);
331
381
  data = texture(materialParams_metallicRoughnessMap, uv);
332
382
  }
333
383
 
384
+ // a frame's bark fraction (the occlusion map's r; g = 0 says the map is there — the default white reads 0)
385
+ float impBark(highp vec2 uv) {
386
+ vec2 o = texture(materialParams_occlusionMap, uv).rg;
387
+ return o.r * (1.0 - o.g);
388
+ }
389
+ // a frame's bark occlusion (the occlusion map's b; 0 = an atlas without the channel, and the default white, read 1)
390
+ float impBarkOcc(highp vec2 uv) {
391
+ float b = texture(materialParams_occlusionMap, uv).b;
392
+ return b > 0.0 ? b : 1.0;
393
+ }
394
+
334
395
  // the sky's SH irradiance on n (foliage-cards.mat's shIrradianceV; Filament's own is not in every variant's fragment)
335
396
  vec3 impSky(const vec3 n) {
336
397
  return max(
@@ -386,9 +447,10 @@ fragment {
386
447
 
387
448
  vec4 b0, b1, b2, t0, t1, t2;
388
449
  highp vec3 d0, d1, d2, r0, r1, r2, u0, u1, u2;
389
- impFrame(fr.x, isGrid, cells, r, p, inset, b0, t0, d0, r0, u0);
390
- impFrame(fr.y, isGrid, cells, r, p, inset, b1, t1, d1, r1, u1);
391
- impFrame(fr.z, isGrid, cells, r, p, inset, b2, t2, d2, r2, u2);
450
+ highp vec2 uv0, uv1, uv2;
451
+ impFrame(fr.x, isGrid, cells, r, p, inset, b0, t0, uv0, d0, r0, u0);
452
+ impFrame(fr.y, isGrid, cells, r, p, inset, b1, t1, uv1, d1, r1, u1);
453
+ impFrame(fr.z, isGrid, cells, r, p, inset, b2, t2, uv2, d2, r2, u2);
392
454
  vec4 base = (b0 * w.x + b1 * w.y + b2 * w.z) * materialParams.baseColorFactor;
393
455
  // the distance fade thins the tree like the cards (the mask threshold does the cutting)
394
456
  base.a *= variable_impW.y;
@@ -398,7 +460,12 @@ fragment {
398
460
  if (base.a >= 0.5 * cut) base.a = max(base.a, cut);
399
461
  material.baseColor = base;
400
462
  #else
463
+ // the coverage boost (the header: the atlas' own mip past the first, the cards' `look.w`): a far crown keeps its share
464
+ base.a = min(base.a * (1.0 + materialParams.look.w * variable_impW.z), 1.0);
401
465
  vec4 data = t0 * w.x + t1 * w.y + t2 * w.z;
466
+ float bark = clamp(impBark(uv0) * w.x + impBark(uv1) * w.y + impBark(uv2) * w.z, 0.0, 1.0);
467
+ gBark = bark;
468
+ float barkOcc = impBarkOcc(uv0) * w.x + impBarkOcc(uv1) * w.y + impBarkOcc(uv2) * w.z;
402
469
  highp vec3 nObj = impNormal(t0) * w.x + impNormal(t1) * w.y + impNormal(t2) * w.z;
403
470
  highp float nl = length(nObj);
404
471
  nObj = nl > 1e-5 ? nObj / nl : d0;
@@ -417,8 +484,11 @@ fragment {
417
484
  // THE LIGHT, foliage-cards.mat's per-vertex model at the pixel: the sky's SH on the crown's normal darkened by the
418
485
  // baked sky visibility (leaf.w), and the sun's wrapped Lambert + translucency for surfaceShading()
419
486
  float vao = data.a;
420
- float ao = mix(1.0, vao, clamp(materialParams.leaf.w, 0.0, 1.0));
421
- vec3 ambient = impSky(n) * frameUniforms.iblLuminance * ao;
487
+ // the bark's share also takes its own occlusion in full (foliage.mat: occ x cavity on the ambient, the sky
488
+ // visibility alone by leaf.w) - measured 2026-11-04 on a Chernobyl pine: without it the quad's trunk read 15 % brighter
489
+ float ao = mix(1.0, vao, clamp(materialParams.leaf.w, 0.0, 1.0)) * mix(1.0, barkOcc, bark);
490
+ // + the sky through the leaf on the cards' share (foliage-cards.mat look.x), the bark's share opaque
491
+ vec3 ambient = (impSky(n) + materialParams.look.x * (1.0 - bark) * impSky(-n)) * frameUniforms.iblLuminance * ao;
422
492
  highp vec3 l = frameUniforms.lightDirection;
423
493
  highp vec3 v = getWorldViewVector();
424
494
  float wrap = clamp(materialParams.sway.w, 0.0, 1.0);
@@ -426,7 +496,8 @@ fragment {
426
496
  float back = max(-dot(n, l), 0.0);
427
497
  float vl = pow(max(dot(-v, l), 0.0), 4.0);
428
498
  float through = clamp(materialParams.leaf.z, 0.0, 1.0) * back * (0.5 + 0.5 * vl) * mix(1.0, vao, 0.5);
429
- gSun = NoL + through;
499
+ // the bark's share: a plain Lambert on its own normal (foliage.mat's opaque variant: no wrap, no translucency)
500
+ gSun = mix(NoL + through, max(dot(n, l), 0.0), bark);
430
501
 
431
502
  // THE SELF-SHADOW from the atlas: the frames around the sun are the tree's own shadow map (the header). The
432
503
  // pixel's surface point (offset from c, object space), the three view frames' points blended by their weights:
@@ -463,8 +534,8 @@ fragment {
463
534
  bool inSun = lo.y >= 0.0; // the sun above the horizon
464
535
  float selfShadow = inSun ? 1.0 - occl : 1.0;
465
536
  // by the cards' share of the shadow map (leaf.y, foliage-cards.mat surfaceShading: a crown never goes black under its
466
- // own shadow), and the knob
467
- selfShadow = mix(1.0, selfShadow, materialParams.impostorSelfShadow * clamp(materialParams.leaf.y, 0.0, 1.0));
537
+ // own shadow) — the bark's in full —, and the knob
538
+ selfShadow = mix(1.0, selfShadow, materialParams.impostorSelfShadow * mix(clamp(materialParams.leaf.y, 0.0, 1.0), 1.0, bark));
468
539
  gSun *= selfShadow;
469
540
 
470
541
  material.baseColor = base;
@@ -473,12 +544,18 @@ fragment {
473
544
  material.reflectance = 0.0;
474
545
  material.ambientOcclusion = 0.0; // Filament's IBL off: the ambient is ours, as emissive
475
546
  material.emissive = vec4(base.rgb * ambient, 0.0);
547
+ // THE DEBUG VIEW (foliage-cards.mat's): 1 = the albedo, 2 = the hull normal (world) ×½+½, 3 = the sky term on
548
+ // white; the sun off (surfaceShading returns 0), the alpha untouched
549
+ float dbg = materialParams.debugMode;
550
+ if (dbg > 0.5) material.emissive = vec4(dbg < 1.5 ? base.rgb : dbg < 2.5 ? n * 0.5 + 0.5 : ambient, 0.0);
476
551
  #endif
477
552
  }
478
553
 
479
554
  // PER LIGHT (the sun first): the pixel's sun scalar times the light and its SHADOW, by leaf.y — foliage-cards.mat's rule
555
+ // — on the cards' share, in full on the bark's (foliage.mat)
480
556
  vec3 surfaceShading(const MaterialInputs materialInputs, const ShadingData shadingData, const LightData lightData) {
481
- float lit = mix(1.0, lightData.visibility, clamp(materialParams.leaf.y, 0.0, 1.0));
557
+ if (materialParams.debugMode > 0.5) return vec3(0.0); // the debug view: no sun
558
+ float lit = mix(1.0, lightData.visibility, mix(clamp(materialParams.leaf.y, 0.0, 1.0), 1.0, gBark));
482
559
  float reach = lightData.colorIntensity.w * lightData.attenuation * lit;
483
560
  return shadingData.diffuseColor * lightData.colorIntensity.rgb * (reach * gSun / PI);
484
561
  }
@@ -97,7 +97,10 @@ material {
97
97
  // per bender: xy = travel direction on the ground (unit, or 0 when still), z = speed weight 0..1
98
98
  // (creator-gl smooths the velocity over ~0.2 s) — near a plant's centre the lean follows the
99
99
  // travel direction instead of the radial one, so crossing a bush never flips it (2026-09-12)
100
- { type : float4[8], name : benderMotion }
100
+ { type : float4[8], name : benderMotion },
101
+ // the impostor lab's DEBUG VIEW (glb.cpp CREATOR_FOLIAGE_DEBUG; 0 = off): 1 albedo, 2 the crown's hull normal as
102
+ // colour (what the impostor atlas stores for every texel, bark included), 3 the sky term on white — the sun off
103
+ { type : float, name : debugMode }
101
104
  ]
102
105
  }
103
106
 
@@ -192,6 +195,13 @@ fragment {
192
195
  material.reflectance = 0.35; // a faint sheen on the bark under grazing light (f0 ≈ 0.02; the lobe is in surfaceShading)
193
196
  material.ambientOcclusion = 0.0; // Filament's IBL off (the header of lightmap.mat)
194
197
  material.emissive = vec4(color + glow, 0.0);
198
+ float dbg = materialParams.debugMode;
199
+ if (dbg > 0.5) {
200
+ vec3 hn = variable_radial.xyz;
201
+ float hl = length(hn);
202
+ hn = hl > 1e-5 ? hn / hl : vec3(0.0, 1.0, 0.0);
203
+ material.emissive = vec4(dbg < 1.5 ? base.rgb : dbg < 2.5 ? hn * 0.5 + 0.5 : shIrradiance(ns) * ao * frameUniforms.iblLuminance, 0.0);
204
+ }
195
205
  }
196
206
 
197
207
  // PER LIGHT (Filament's loop: the sun first, then the lamps in range), with the light's shadow in `visibility` -
@@ -201,6 +211,7 @@ fragment {
201
211
  // faces the viewer, double-sided) narrowed toward the view line into the light, as HDRP's thin-surface transmission
202
212
  // does.
203
213
  vec3 surfaceShading(const MaterialInputs materialInputs, const ShadingData shadingData, const LightData lightData) {
214
+ if (materialParams.debugMode > 0.5) return vec3(0.0); // the debug view: no sun
204
215
  vec3 ns = gNs;
205
216
  vec3 v = getWorldViewVector();
206
217
  vec3 l = lightData.l;
@@ -144,9 +144,12 @@ vertex {
144
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
145
  // the open sky is reached at the crowns' level, CANOPY metres up
146
146
  up = clamp((f.uwp.y - f.uOrigin.y) / CANOPY, 0.0, 1.0);
147
- ambient = mix(groundAmbient, skyAmbient, up);
148
- }
149
- ambient *= ao;
147
+ // the ground's share takes NO vertex AO: the terrain texel under the plant already carries the plant's own
148
+ // baked shadow (a fern's centre read its self-occlusion three times over - the ground's E, the down-facing
149
+ // hull's quarter and its own vao - and went black under an overcast sky, 2026-11-04); the open sky's share
150
+ // keeps it, that is the vertex's own depth in the plant
151
+ ambient = mix(groundAmbient, skyAmbient * ao, up);
152
+ } else ambient *= ao;
150
153
  vec3 l = frameUniforms.lightDirection;
151
154
  vec3 v = normalize(getWorldCameraPosition() - f.wp);
152
155
  float wrap = clamp(materialParams.sway.w, 0.0, 1.0);