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.
- package/dist/index.js +2681 -1196
- package/package.json +10 -10
- package/runtime/materials/decal-relief.mat +21 -46
- package/runtime/materials/decal.mat +41 -61
- package/runtime/materials/foliage-cards.mat +28 -140
- package/runtime/materials/foliage-impostor.mat +485 -0
- package/runtime/materials/foliage.mat +232 -0
- package/runtime/materials/grass.mat +244 -0
- package/runtime/materials/particles-quad.mat +5 -3
- package/runtime/materials/particles.mat +5 -3
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/materials-Cs_z1zLi.bin +0 -0
- package/runtime/web/assets/materials-world-BtoAfznA.bin +0 -0
- package/runtime/web/player.js +3 -3
- package/runtime/web-lite/assets/{canvas-WxiusXCB.js → canvas-wFjS40IX.js} +1 -1
- package/runtime/web-lite/assets/creator2d-1pSkYRCn.js +1 -0
- package/runtime/web-lite/assets/creator2d-Ci5-pZGr.wasm +0 -0
- package/runtime/web-lite/assets/{pager-DpGa2uaE.js → pager-BZAO9gMC.js} +1 -1
- package/runtime/web-lite/assets/{plugins-CuEPHbHL.js → plugins-B7TuO_p4.js} +1 -1
- package/runtime/web-lite/assets/scene2d-F6T1Io7I.js +1 -0
- package/runtime/web-lite/assets/{tween-BnWYhJ7P.js → tween-DbrQTld4.js} +1 -1
- package/runtime/web-lite/assets/{video-BLj61nDx.js → video-bzR_t2i6.js} +1 -1
- package/runtime/web-lite/assets/{vlist-CwSYpLz-.js → vlist-Ap-F4H5J.js} +1 -1
- package/runtime/web-lite/features.json +18 -6
- package/runtime/web-lite/lite.js +6 -6
- package/src/commands/compile.ts +2 -1
- package/src/commands/hub/create.ts +100 -0
- package/src/commands/hub/credential.ts +32 -0
- package/src/commands/hub/index.ts +21 -0
- package/src/commands/hub/login.ts +89 -0
- package/src/commands/hub/status.ts +109 -0
- package/src/commands/index.ts +5 -1
- package/src/commands/level.ts +123 -0
- package/src/commands/lightmap.ts +41 -76
- package/src/commands/navmesh.ts +84 -67
- package/src/commands/secrets.ts +147 -0
- package/src/commands/update/index.ts +154 -64
- package/src/compile/designCompile.ts +2 -0
- package/src/compile/headlessBundle.ts +2 -0
- package/src/compile/projectCompile.ts +12 -1
- package/src/compile/sceneCompile.ts +2 -1
- package/src/dev/backendWorker.ts +6 -1
- package/src/dev/devServer.ts +2 -1
- package/src/dev/localBackend.ts +21 -14
- package/src/dev/projectBackend.ts +7 -3
- package/src/dev/watchProject.ts +2 -0
- package/src/hosts/desktopRenderer.ts +10 -2
- package/src/hosts/peerInstall.ts +26 -6
- package/src/hub/api.ts +220 -0
- package/src/hub/config.ts +53 -0
- package/src/hub/git.ts +148 -0
- package/src/platform/api.ts +21 -0
- package/src/project/env.ts +29 -0
- package/src/web/build.ts +149 -88
- package/runtime/web/assets/materials-CJzxzdUK.bin +0 -0
- 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
|
+
}
|