lecodes-cli 2.4.9 → 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 +2681 -1196
  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 -76
  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 +26 -6
  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,485 @@
1
+ // foliage-impostor: a FAR TREE as one camera-facing quad, textured from an ATLAS of views of the tree that lecodes-assets
2
+ // bakes at import into the tree's own GLB as its last LOD level (`<root>_LOD<k>`; creator-gl lod.cpp shows it like any
3
+ // other level). The atlas holds only what does not depend on the light; this material lights it at runtime with the
4
+ // crown cards' model (foliage-cards.mat), per pixel instead of per vertex.
5
+ //
6
+ // THE ATLAS (the baker's half of the contract; both halves must agree bit for bit). Two frame LAYOUTS, told apart by
7
+ // `impostorGrid` = (A, E) with E > 0 for the GRID, (N, 0) for the OCTAHEDRON; both cover only the upper hemisphere (+Y)
8
+ // of the GLB's object space — a tree is never seen from below.
9
+ // • THE GRID (the default, 12 × 3): A azimuths × E elevation rows (A ≥ 3, E ≥ 1). Frame (i, k), i ∈ [0, A), k ∈ [0, E)
10
+ // looks at the tree from az_i = i·2π/A, el_k = k·(π/2)/E (E = 3 → 0°, 30°, 60°; 90° is never a row, the top row is
11
+ // reused above it), the direction d = (cos el·sin az, sin el, cos el·cos az) — az = 0 looks from +Z, 90° from +X.
12
+ // Encode: az = atan(d.x, d.z) wrapped into [0, 2π) (0 when d.x = d.z = 0, the zenith), el = asin(clamp(d.y, 0, 1));
13
+ // fa = az/2π·A (frame A ≡ frame 0), fe = clamp(el·E/(π/2), 0, E-1). Packed frame index i·E + k.
14
+ // Atlas A·P × E·P texels (P a frame), frame (i, k) at x ∈ [i·P, (i+1)·P), y ∈ [k·P, (k+1)·P): ROW 0 = elevation 0
15
+ // at the TOP of the image.
16
+ // • THE OCTAHEDRON: N×N frames on a HEMI-octahedron. Frame (i, j) looks from the direction decode((i, j) / (N-1)):
17
+ // decode(uv): u = 2uv.x-1, v = 2uv.y-1, x = (u+v)/2, z = (u-v)/2, y = 1-|x|-|z|, normalised
18
+ // encode(d): s = |x|+|y|+|z|, u = (x+z)/s, v = (x-z)/s, back to [0,1]
19
+ // the zenith is the map's centre, every point of its border the horizon. Packed frame index i·N + j. Atlas N·P
20
+ // square, frame (i, j) at x ∈ [i·P, (i+1)·P), y ∈ [j·P, (j+1)·P).
21
+ // • each frame is an orthographic view of the square [-r, r]² around the level-0 bounding sphere's centre c
22
+ // (`impostor` = (c.xyz, r), object space), with the basis of its direction d (the same for both layouts):
23
+ // ref = |d.y| < 0.999 ? +Y : -Z, right = normalize(cross(ref, d)), up = cross(d, right)
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
+ // 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
+ // 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 —
30
+ // 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
+ // crown's shading; b = the DEPTH along the frame's view axis, z = dot(p - c, d)/r stored z·½+½ (1 = the point
32
+ // 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.
35
+ //
36
+ // THE QUAD: 4 vertices whose POSITION is ignored (the baker writes the sphere's box corners there only so gltfio's
37
+ // culling box is right); TEXCOORD_0 is the corner id, corner = uv·2-1 (x right, y up). The vertex shader takes the
38
+ // direction from the sphere's centre to the eye into object space (uniform scale assumed), clamps it to the upper
39
+ // hemisphere, builds that direction's frame basis and spans the quad over [-r, r]² in it — the quad faces the eye from
40
+ // wherever it is looked at. Then the FRAME PICK: the direction falls in a cell of the frame lattice — (fa, fe) on the
41
+ // grid, encode(d)·(N-1) on the octahedron; with (i, k) = its floor and t = its fract, the cell's lower triangle
42
+ // (t.x + t.y < 1) names the frames (i,k) (i+1,k) (i,k+1) with the weights (1-t.x-t.y, t.x, t.y), the upper one
43
+ // (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
+ // 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
+ //
46
+ // THE VARYINGS (Filament caps custom variables at 5; four used):
47
+ // impFrames = (frame0, frame1, frame2, hash) — a frame packed as i·E + k (grid) / i·N + j (octahedron); hash = the
48
+ // cards' per-instance tint hash
49
+ // 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)
51
+ // impRot = the model's rotation as a quaternion (its columns normalised): getWorldFromModelMatrix() exists in the
52
+ // vertex stage only, and the fragment needs it to take the atlas' object-space normal to the world
53
+ // The frame choice and the weights depend on the CENTRE's direction alone, so they are the same at the four corners:
54
+ // the interpolation hands them over unchanged.
55
+ //
56
+ // THE FRAGMENT: per frame, unpack its index (grid: i = floor(packed/E), k = packed - i·E; octahedron: by N), decode its
57
+ // 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
60
+ // in object space, no basis —, blended, normalised, turned to the world by the quaternion. THE LIGHT is
61
+ // 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
+ // 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
64
+ // also becomes Filament's shading normal, the one its lights loop tests (dot(n, l) > 0) before it reads the shadow map.
65
+ //
66
+ // THE SELF-SHADOW: a flat quad never shades itself through the shadow map, and at these distances the mesh's own
67
+ // self-shading IS the shadow map (upper branches over the lower ones) — so the impostor shadows itself from its OWN
68
+ // ATLAS: the frames around the sun are its shadow map. The pixel's surface point (offset from c, object space): per view
69
+ // frame k, p's point on that frame's plane (dot(p, right_k)·right_k + dot(p, up_k)·up_k) plus its depth z_k·r·d_k,
70
+ // blended by the weights. The sun in object space (the quaternion's inverse on frameUniforms.lightDirection, which
71
+ // points TOWARD the light) names the sun frames. A sun frame's occluder (impOccl): the point projected onto its basis
72
+ // gives (a_s, b_s) and its depth z_p; the frame's coverage and depth there (2 fetches, the view frames' half-texel
73
+ // inset) give occl = coverage · smoothstep(0, bias, z_o - z_p - bias) with bias = 6/P (P = a frame's texels across:
74
+ // ≈ 3 texels, ≥ 2 depth steps at P = 256), 0 where the point falls outside that frame's square. On the GRID the sun
75
+ // takes the nearest azimuth, i = round(az/2π·A) mod A, and BLENDS the two elevation rows around it: k0 = floor(fe),
76
+ // k1 = min(k0 + 1, E-1), occl = mix(occl(i, k0), occl(i, k1), fract(fe)) — 4 fetches. The nearest row alone was
77
+ // measured against the mesh: a sun between two rows (the Forest's 48.7°) took the 60° row, which throws the top tier's
78
+ // shadow straight down and over-darkens the upper-middle crown; the blend weighs both rows' shadows. The OCTAHEDRON
79
+ // keeps the NEAREST single frame, round(encode(sun)·(N-1)) (its second call names the same frame with a blend of 0,
80
+ // so its fetches stay unconditional). gSun — the whole sun scalar, Lambert and translucency, as the cards' is under the shadow map — is
81
+ // multiplied by 1 - occl. The term is 1 with the sun under the horizon; `impostorSelfShadow` mixes it in (1 = on;
82
+ // glb.cpp writes the 1, CREATOR_IMPOSTOR_SELFSHADOW=0 a 0). The depth variants (the shadow map, the prepass) skip it.
83
+ //
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
86
+ // with the bark and the cards, and the GLB provider picks it for a material whose extras carry `lecodes.impostor`
87
+ // ([A, cx, cy, cz, r, E] → impostorGrid = (A, E) / impostor; E = 0 or missing = the octahedron, N = the first number).
88
+
89
+ material {
90
+ name : foliage_impostor,
91
+ shadingModel : lit,
92
+ customSurfaceShading : true,
93
+ specularAmbientOcclusion : simple,
94
+ doubleSided : true,
95
+ culling : none,
96
+ blending : masked,
97
+ alphaToCoverage : true,
98
+ flipUV : false,
99
+ requires : [ uv0 ],
100
+ variables : [
101
+ { name : impFrames, precision : high },
102
+ { name : impPos, precision : high },
103
+ { name : impW, precision : high },
104
+ impRot
105
+ ],
106
+ parameters : [
107
+ { type : float4, name : baseColorFactor },
108
+ { type : sampler2d, name : baseColorMap },
109
+ { type : float, name : metallicFactor },
110
+ { type : float, name : roughnessFactor },
111
+ { type : float3, name : emissiveFactor },
112
+ { type : float, name : normalScale },
113
+ { type : float, name : aoStrength },
114
+ { type : sampler2d, name : normalMap },
115
+ { type : float, name : hasNormalMap },
116
+ { type : sampler2d, name : metallicRoughnessMap },
117
+ { type : sampler2d, name : occlusionMap },
118
+ { type : sampler2d, name : emissiveMap },
119
+ // ---- foliage (foliage.mat has every comment; the engine writes the block to any material with `benders`) ----
120
+ { type : float4, name : wind },
121
+ { type : float4, name : sway },
122
+ { type : float4, name : fade },
123
+ { type : float4, name : leaf },
124
+ { type : float4, name : profile },
125
+ { type : float, name : shiverDir },
126
+ { type : float4, name : look },
127
+ { type : sampler2d, name : windNoise },
128
+ { type : float4, name : plant },
129
+ { type : float4[8], name : benders },
130
+ { type : float4[8], name : benderMotion },
131
+ // ---- the impostor (the GLB material's extras `lecodes.impostor` = [A, cx, cy, cz, r, E]) ----
132
+ { type : float4, name : impostor, precision : high }, // (c.xyz, r): the level-0 bounding sphere, object space
133
+ { 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)
135
+ ]
136
+ }
137
+
138
+ vertex {
139
+ // the hemi-octahedral map, [0,1]² <-> a unit direction with y ≥ 0 (the baker's TS is the same formula)
140
+ highp vec3 impDecode(highp vec2 uv) {
141
+ highp float u = uv.x * 2.0 - 1.0;
142
+ highp float v = uv.y * 2.0 - 1.0;
143
+ highp float x = (u + v) * 0.5;
144
+ highp float z = (u - v) * 0.5;
145
+ return normalize(vec3(x, 1.0 - abs(x) - abs(z), z));
146
+ }
147
+
148
+ highp vec2 impEncode(highp vec3 d) {
149
+ highp float s = abs(d.x) + abs(d.y) + abs(d.z);
150
+ highp float px = d.x / s;
151
+ highp float pz = d.z / s;
152
+ return vec2(px + pz, px - pz) * 0.5 + 0.5;
153
+ }
154
+
155
+ // the rotation part of the model matrix (its columns normalised) as a unit quaternion (x, y, z, w); R[c][r] = m_rc
156
+ vec4 impQuat(mat3 R) {
157
+ float tr = R[0][0] + R[1][1] + R[2][2];
158
+ vec4 q;
159
+ if (tr > 0.0) {
160
+ float s = sqrt(tr + 1.0) * 2.0;
161
+ q = vec4(R[1][2] - R[2][1], R[2][0] - R[0][2], R[0][1] - R[1][0], 0.25 * s * s) / s;
162
+ } else if (R[0][0] > R[1][1] && R[0][0] > R[2][2]) {
163
+ float s = sqrt(1.0 + R[0][0] - R[1][1] - R[2][2]) * 2.0;
164
+ q = vec4(0.25 * s * s, R[1][0] + R[0][1], R[2][0] + R[0][2], R[1][2] - R[2][1]) / s;
165
+ } else if (R[1][1] > R[2][2]) {
166
+ float s = sqrt(1.0 + R[1][1] - R[0][0] - R[2][2]) * 2.0;
167
+ q = vec4(R[1][0] + R[0][1], 0.25 * s * s, R[2][1] + R[1][2], R[2][0] - R[0][2]) / s;
168
+ } else {
169
+ float s = sqrt(1.0 + R[2][2] - R[0][0] - R[1][1]) * 2.0;
170
+ q = vec4(R[2][0] + R[0][2], R[2][1] + R[1][2], 0.25 * s * s, R[0][1] - R[1][0]) / s;
171
+ }
172
+ return normalize(q);
173
+ }
174
+
175
+ void materialVertex(inout MaterialVertexInputs material) {
176
+ highp mat4 model = getWorldFromModelMatrix();
177
+ highp vec4 imp = materialParams.impostor;
178
+ highp vec2 G = materialParams.impostorGrid;
179
+ bool isGrid = G.y > 0.0;
180
+ highp float N = max(floor(G.x + 0.5), 1.0); // the grid's A, or the octahedron's N
181
+ highp float E = isGrid ? max(floor(G.y + 0.5), 1.0) : N; // the grid's rows (the octahedron: N again)
182
+ highp float r = imp.w;
183
+ highp vec3 c = imp.xyz;
184
+ highp vec2 corner = material.uv0 * 2.0 - 1.0;
185
+
186
+ // THE VIEW DIRECTION, from the sphere's centre to the eye, in object space (the transpose undoes the rotation; a
187
+ // uniform scale only stretches it) and clamped to the upper hemisphere the atlas covers
188
+ highp vec3 cW = (model * vec4(c, 1.0)).xyz;
189
+ highp vec3 dW = getWorldCameraPosition() - cW;
190
+ highp vec3 d = transpose(mat3(model)) * dW;
191
+ d.y = max(d.y, 0.0);
192
+ highp float dl = length(d);
193
+ d = dl > 1e-6 ? d / dl : vec3(0.0, 1.0, 0.0);
194
+
195
+ // the quad spans [-r, r]² in that direction's frame basis
196
+ highp vec3 ref = abs(d.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(0.0, 0.0, -1.0);
197
+ highp vec3 right = normalize(cross(ref, d));
198
+ highp vec3 up = cross(d, right);
199
+ highp vec3 p = (corner.x * right + corner.y * up) * r;
200
+ material.worldPosition.xyz = (model * vec4(c + p, 1.0)).xyz;
201
+ #if defined(HAS_ATTRIBUTE_TANGENTS)
202
+ // the quad's geometric normal faces the eye (the shadow lookup's normal bias reads it; the mesh's NORMAL is a dummy)
203
+ highp float dwl = length(dW);
204
+ material.worldNormal = dwl > 1e-6 ? dW / dwl : vec3(0.0, 1.0, 0.0);
205
+ #endif
206
+
207
+ // THE FRAME PICK: the cell of the frame lattice the direction falls in, its lower or upper triangle, barycentric
208
+ // weights. The grid: (fa, fe) = (azimuth / 2π · A, elevation / (π/2) · E clamped to the top row); the octahedron:
209
+ // encode(d)·(N-1).
210
+ highp vec2 f;
211
+ if (isGrid) {
212
+ highp float az = abs(d.x) + abs(d.z) > 1e-7 ? atan(d.x, d.z) : 0.0; // the zenith has no azimuth: 0
213
+ if (az < 0.0) az += 2.0 * PI;
214
+ highp float el = asin(clamp(d.y, 0.0, 1.0));
215
+ f = vec2(az / (2.0 * PI) * N, clamp(el * E / (0.5 * PI), 0.0, E - 1.0));
216
+ } else {
217
+ f = impEncode(d) * (N - 1.0);
218
+ }
219
+ highp vec2 i = floor(f);
220
+ highp vec2 t = f - i;
221
+ highp vec2 f0, f1, f2;
222
+ highp vec3 w;
223
+ if (t.x + t.y < 1.0) {
224
+ f0 = i; f1 = i + vec2(1.0, 0.0); f2 = i + vec2(0.0, 1.0);
225
+ w = vec3(1.0 - t.x - t.y, t.x, t.y);
226
+ } else {
227
+ f0 = i + vec2(1.0, 1.0); f1 = i + vec2(0.0, 1.0); f2 = i + vec2(1.0, 0.0);
228
+ w = vec3(t.x + t.y - 1.0, 1.0 - t.x, 1.0 - t.y);
229
+ }
230
+ if (isGrid) {
231
+ // the azimuth wraps (frame A ≡ frame 0; i itself is A when az rounds up to 2π), the rows stop at the top one
232
+ f0.x = f0.x >= N ? f0.x - N : f0.x; f1.x = f1.x >= N ? f1.x - N : f1.x; f2.x = f2.x >= N ? f2.x - N : f2.x;
233
+ highp float top = E - 1.0;
234
+ f0.y = min(f0.y, top); f1.y = min(f1.y, top); f2.y = min(f2.y, top);
235
+ } else {
236
+ highp float last = N - 1.0;
237
+ f0 = min(f0, vec2(last)); f1 = min(f1, vec2(last)); f2 = min(f2, vec2(last));
238
+ }
239
+
240
+ // 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
+ float hash = fract(sin(dot(uOrigin.xz, vec2(12.9898, 78.233))) * 43758.5453);
243
+ // the distance fade (foliage-wind.vs `alive`, measured to the sphere's centre)
244
+ float fadeEnd = materialParams.fade.y;
245
+ float alive = fadeEnd > 0.0 ? 1.0 - smoothstep(materialParams.fade.x, fadeEnd, length(dW)) : 1.0;
246
+
247
+ // packed i·E + k (grid) / i·N + j (octahedron: E = N)
248
+ material.impFrames = vec4(f0.x * E + f0.y, f1.x * E + f1.y, f2.x * E + f2.y, hash);
249
+ material.impPos = vec4(p, w.x);
250
+ material.impW = vec4(w.y, alive, 0.0, 0.0);
251
+ mat3 R = mat3(normalize(model[0].xyz), normalize(model[1].xyz), normalize(model[2].xyz));
252
+ material.impRot = impQuat(R);
253
+ }
254
+ }
255
+
256
+ fragment {
257
+ // from material() for surfaceShading(): the sun's scalar at this pixel — the wrapped Lambert + the translucency
258
+ float gSun = 0.0;
259
+
260
+ highp vec3 impDecode(highp vec2 uv) {
261
+ highp float u = uv.x * 2.0 - 1.0;
262
+ highp float v = uv.y * 2.0 - 1.0;
263
+ highp float x = (u + v) * 0.5;
264
+ highp float z = (u - v) * 0.5;
265
+ return normalize(vec3(x, 1.0 - abs(x) - abs(z), z));
266
+ }
267
+
268
+ // the hemi-octahedral frame map's encode (the vertex stage's): the octahedron's frame nearest the sun
269
+ highp vec2 impEncode(highp vec3 d) {
270
+ highp float s = abs(d.x) + abs(d.y) + abs(d.z);
271
+ highp float px = d.x / s;
272
+ highp float pz = d.z / s;
273
+ return vec2(px + pz, px - pz) * 0.5 + 0.5;
274
+ }
275
+
276
+ // the data map's normal (rg): the FULL-sphere octahedral map, [0,1]² -> a unit object-space vector; the baker's
277
+ // normalEncode is its inverse. Not impEncode/impDecode — those are the frame layout's hemi map.
278
+ // THE SEAM POLE IS OBJECT −Y: the baker encodes the swapped vector (n.x, n.z, n.y), so the mapping's "z" (the fold
279
+ // test, the pole at the four corners) is object y, and the decode swaps y and z back. A view frame never looks from
280
+ // straight below, so no frame's normals straddle the corners, where bilinear and mips average (1,1) with (-1,1)
281
+ // into garbage (with the pole at −Z they did: every near-side normal of the az-180° frame). Anchors: (½,½) = +Y,
282
+ // the corners = −Y, (½,1) = +Z, (½,0) = −Z, (1,½) = +X.
283
+ highp vec3 normalDecode(highp vec2 e) {
284
+ highp vec2 p = e * 2.0 - 1.0;
285
+ highp vec3 n = vec3(p.x, p.y, 1.0 - abs(p.x) - abs(p.y));
286
+ highp float t = clamp(-n.z, 0.0, 1.0);
287
+ n.x += n.x >= 0.0 ? -t : t;
288
+ n.y += n.y >= 0.0 ? -t : t;
289
+ return normalize(n.xzy);
290
+ }
291
+
292
+ // Frame (i, k) of either layout: its direction d and basis (right, up). `cells` = the atlas' frames across and
293
+ // down: (A, E) on the grid, (N, N) on the octahedron.
294
+ void impBasis(highp float fi, highp float fk, bool isGrid, highp vec2 cells,
295
+ out highp vec3 dk, out highp vec3 rk, out highp vec3 uk) {
296
+ if (isGrid) {
297
+ // the grid's frame (i, k): az = i·2π/A, el = k·(π/2)/E
298
+ highp float az = fi * (2.0 * PI) / cells.x;
299
+ highp float el = fk * (0.5 * PI) / cells.y;
300
+ highp float ce = cos(el);
301
+ dk = vec3(ce * sin(az), sin(el), ce * cos(az));
302
+ } else {
303
+ dk = impDecode(vec2(fi, fk) / max(cells.x - 1.0, 1.0));
304
+ }
305
+ highp vec3 ref = abs(dk.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(0.0, 0.0, -1.0);
306
+ rk = normalize(cross(ref, dk));
307
+ uk = cross(dk, rk);
308
+ }
309
+
310
+ // The atlas uv of the frame-local point (a, b) ∈ [-1, 1]² (a right, b up) in frame (i, k), clamped half a texel
311
+ // inside the frame, so a point the frame's square does not reach (p projected onto ANOTHER view's basis can leave
312
+ // it) never reads the neighbouring frame.
313
+ highp vec2 impUv(highp float fi, highp float fk, highp vec2 cells, highp float a, highp float b, highp vec2 inset) {
314
+ highp float ua = clamp(0.5 + 0.5 * a, inset.x, 1.0 - inset.x);
315
+ highp float ub = clamp(0.5 + 0.5 * b, inset.y, 1.0 - inset.y);
316
+ return vec2((fi + ua) / cells.x, (fk + 1.0 - ub) / cells.y);
317
+ }
318
+
319
+ // One frame of the atlas at the object-space offset p: the albedo + coverage, the data texel, and the frame's
320
+ // direction and basis. The packed index is a whole number (rounded by the caller); the +½ cell bias only guards the
321
+ // division against a GPU's approximate one.
322
+ // PARALLAX (the next step, not built): the data map's depth (b) exists now — the self-shadow reads it; offsetting p
323
+ // along the view by it before the projection would line the three frames up and cut the ghosting between them.
324
+ 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) {
326
+ highp float fi = floor(packed / cells.y + 0.5 / cells.y);
327
+ highp float fk = packed - fi * cells.y;
328
+ 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);
330
+ base = texture(materialParams_baseColorMap, uv);
331
+ data = texture(materialParams_metallicRoughnessMap, uv);
332
+ }
333
+
334
+ // the sky's SH irradiance on n (foliage-cards.mat's shIrradianceV; Filament's own is not in every variant's fragment)
335
+ vec3 impSky(const vec3 n) {
336
+ return max(
337
+ frameUniforms.iblSH[0].xyz
338
+ + frameUniforms.iblSH[1].xyz * n.y + frameUniforms.iblSH[2].xyz * n.z + frameUniforms.iblSH[3].xyz * n.x
339
+ + frameUniforms.iblSH[4].xyz * (n.y * n.x) + frameUniforms.iblSH[5].xyz * (n.y * n.z)
340
+ + frameUniforms.iblSH[6].xyz * (3.0 * n.z * n.z - 1.0) + frameUniforms.iblSH[7].xyz * (n.z * n.x)
341
+ + frameUniforms.iblSH[8].xyz * (n.x * n.x - n.y * n.y), 0.0);
342
+ }
343
+
344
+ // a frame's hull normal, object space (the data map's rg)
345
+ highp vec3 impNormal(vec4 data) {
346
+ return normalDecode(data.rg);
347
+ }
348
+
349
+ // a frame's surface point, as an offset from c in object space: p's point on the frame's plane plus the data map's
350
+ // depth (b) along its direction
351
+ highp vec3 impSurface(highp vec3 p, vec4 data, highp float r, highp vec3 dk, highp vec3 rk, highp vec3 uk) {
352
+ return dot(p, rk) * rk + dot(p, uk) * uk + (data.b * 2.0 - 1.0) * r * dk;
353
+ }
354
+
355
+ // The self-shadow's occluder from ONE sun frame (si, sk): the surface point P (offset from c, object space)
356
+ // projected onto the frame's basis, the frame's coverage and depth there (2 fetches, always taken — no flow control
357
+ // around them, so the derivatives stay defined), occl = coverage · smoothstep(0, bias, z_o - z_p - bias),
358
+ // bias = 6 / P (P = a frame's texels across; inset = ½ / P). 0 where P falls outside the frame's square.
359
+ float impOccl(highp vec3 P, highp float si, highp float sk, bool isGrid, highp vec2 cells, highp float r,
360
+ highp vec2 inset) {
361
+ highp vec3 ds, rs, us;
362
+ impBasis(si, sk, isGrid, cells, ds, rs, us);
363
+ highp float sa = dot(P, rs) / r;
364
+ highp float sb = dot(P, us) / r;
365
+ highp float zp = dot(P, ds) / r;
366
+ highp vec2 suv = impUv(si, sk, cells, sa, sb, inset);
367
+ float cov = texture(materialParams_baseColorMap, suv).a;
368
+ float zo = texture(materialParams_metallicRoughnessMap, suv).b * 2.0 - 1.0;
369
+ float bias = 12.0 * inset.x;
370
+ float occl = cov * smoothstep(0.0, bias, zo - zp - bias);
371
+ return abs(sa) <= 1.0 && abs(sb) <= 1.0 ? occl : 0.0;
372
+ }
373
+
374
+ void material(inout MaterialInputs material) {
375
+ prepareMaterial(material);
376
+ highp vec2 G = materialParams.impostorGrid;
377
+ bool isGrid = G.y > 0.0;
378
+ highp float N = max(floor(G.x + 0.5), 1.0); // A, or the octahedron's N
379
+ highp vec2 cells = vec2(N, isGrid ? max(floor(G.y + 0.5), 1.0) : N); // frames across, down
380
+ highp float r = max(materialParams.impostor.w, 1e-4);
381
+ highp vec3 p = variable_impPos.xyz;
382
+ highp vec3 fr = floor(variable_impFrames.xyz + 0.5);
383
+ highp vec3 w = vec3(variable_impPos.w, variable_impW.x, 1.0 - variable_impPos.w - variable_impW.x);
384
+ // half a texel, in frame-local units, per axis (a frame is width/A texels wide and height/E tall)
385
+ highp vec2 inset = 0.5 * cells / max(vec2(textureSize(materialParams_baseColorMap, 0)), vec2(1.0));
386
+
387
+ vec4 b0, b1, b2, t0, t1, t2;
388
+ 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);
392
+ vec4 base = (b0 * w.x + b1 * w.y + b2 * w.z) * materialParams.baseColorFactor;
393
+ // the distance fade thins the tree like the cards (the mask threshold does the cutting)
394
+ base.a *= variable_impW.y;
395
+ #if defined(VARIANT_DEPTH)
396
+ // the shadow map / prepass: the cards' rule — a texel near the cutoff casts (no coverage boost here)
397
+ float cut = getMaskThreshold();
398
+ if (base.a >= 0.5 * cut) base.a = max(base.a, cut);
399
+ material.baseColor = base;
400
+ #else
401
+ vec4 data = t0 * w.x + t1 * w.y + t2 * w.z;
402
+ highp vec3 nObj = impNormal(t0) * w.x + impNormal(t1) * w.y + impNormal(t2) * w.z;
403
+ highp float nl = length(nObj);
404
+ nObj = nl > 1e-5 ? nObj / nl : d0;
405
+ // object → world by the model's rotation (the quaternion of the vertex stage)
406
+ highp vec4 q = variable_impRot;
407
+ highp vec3 n = normalize(nObj + 2.0 * cross(q.xyz, cross(q.xyz, nObj) + q.w * nObj));
408
+ #if defined(HAS_ATTRIBUTE_TANGENTS)
409
+ shading_normal = n; // Filament's lights loop reads the shadow map only where dot(shading_normal, l) > 0
410
+ #endif
411
+
412
+ // the per-instance tint (foliage-cards.mat)
413
+ float hash = variable_impFrames.w;
414
+ vec3 tint = mix(vec3(0.86, 0.94, 0.72), vec3(1.06, 1.02, 1.12), hash);
415
+ base.rgb *= mix(vec3(1.0), tint, materialParams.sway.z);
416
+
417
+ // 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
+ // baked sky visibility (leaf.w), and the sun's wrapped Lambert + translucency for surfaceShading()
419
+ 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;
422
+ highp vec3 l = frameUniforms.lightDirection;
423
+ highp vec3 v = getWorldViewVector();
424
+ float wrap = clamp(materialParams.sway.w, 0.0, 1.0);
425
+ float NoL = max((dot(n, l) + wrap) / (1.0 + wrap), 0.0);
426
+ float back = max(-dot(n, l), 0.0);
427
+ float vl = pow(max(dot(-v, l), 0.0), 4.0);
428
+ 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;
430
+
431
+ // THE SELF-SHADOW from the atlas: the frames around the sun are the tree's own shadow map (the header). The
432
+ // pixel's surface point (offset from c, object space), the three view frames' points blended by their weights:
433
+ highp vec3 P = impSurface(p, t0, r, d0, r0, u0) * w.x + impSurface(p, t1, r, d1, r1, u1) * w.y
434
+ + impSurface(p, t2, r, d2, r2, u2) * w.z;
435
+ // the sun in object space: the inverse of the model's rotation (the conjugate quaternion) on the world direction
436
+ // toward the light — transpose(mat3(model))·l, normalised
437
+ highp vec3 qi = -q.xyz;
438
+ highp vec3 lo = normalize(l + 2.0 * cross(qi, cross(qi, l) + q.w * l));
439
+ // the sun frames: the grid takes the nearest azimuth and the TWO elevation rows around the sun, blended by the
440
+ // elevation's fraction (the nearest row alone over-darkens the crown when the sun falls between rows); the
441
+ // octahedron keeps the NEAREST single frame (its second frame is the same one, blended by 0)
442
+ highp float si, sk0, sk1;
443
+ float st;
444
+ if (isGrid) {
445
+ highp float saz = abs(lo.x) + abs(lo.z) > 1e-7 ? atan(lo.x, lo.z) : 0.0;
446
+ if (saz < 0.0) saz += 2.0 * PI;
447
+ si = floor(saz / (2.0 * PI) * cells.x + 0.5);
448
+ si = si >= cells.x ? si - cells.x : si;
449
+ highp float sel = asin(clamp(lo.y, 0.0, 1.0));
450
+ highp float fe = clamp(sel * cells.y / (0.5 * PI), 0.0, cells.y - 1.0);
451
+ sk0 = floor(fe);
452
+ sk1 = min(sk0 + 1.0, cells.y - 1.0);
453
+ st = fe - sk0;
454
+ } else {
455
+ highp vec2 sf = clamp(floor(impEncode(lo) * (cells.x - 1.0) + 0.5), vec2(0.0), vec2(cells.x - 1.0));
456
+ si = sf.x; sk0 = sf.y; sk1 = sf.y;
457
+ st = 0.0;
458
+ }
459
+ // both frames' occluders (4 fetches, unconditional: the derivatives stay defined)
460
+ float occl0 = impOccl(P, si, sk0, isGrid, cells, r, inset);
461
+ float occl1 = impOccl(P, si, sk1, isGrid, cells, r, inset);
462
+ float occl = mix(occl0, occl1, st);
463
+ bool inSun = lo.y >= 0.0; // the sun above the horizon
464
+ float selfShadow = inSun ? 1.0 - occl : 1.0;
465
+ // 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));
468
+ gSun *= selfShadow;
469
+
470
+ material.baseColor = base;
471
+ material.metallic = 0.0;
472
+ material.roughness = 1.0;
473
+ material.reflectance = 0.0;
474
+ material.ambientOcclusion = 0.0; // Filament's IBL off: the ambient is ours, as emissive
475
+ material.emissive = vec4(base.rgb * ambient, 0.0);
476
+ #endif
477
+ }
478
+
479
+ // PER LIGHT (the sun first): the pixel's sun scalar times the light and its SHADOW, by leaf.y — foliage-cards.mat's rule
480
+ 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));
482
+ float reach = lightData.colorIntensity.w * lightData.attenuation * lit;
483
+ return shadingData.diffuseColor * lightData.colorIntensity.rgb * (reach * gSun / PI);
484
+ }
485
+ }