lecodes-gl-lite 2.0.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 (46) hide show
  1. package/README.md +27 -0
  2. package/package.json +33 -0
  3. package/src/assets/decoders/README.md +13 -0
  4. package/src/assets/decoders/basis_transcoder.js +19 -0
  5. package/src/assets/decoders/basis_transcoder.wasm +0 -0
  6. package/src/assets/decoders/draco_decoder.wasm +0 -0
  7. package/src/assets/decoders/draco_wasm_wrapper.js +117 -0
  8. package/src/assets/neutral_ibl128.ktx +0 -0
  9. package/src/core/animBridge.ts +51 -0
  10. package/src/core/boneNames.ts +22 -0
  11. package/src/core/frame.ts +230 -0
  12. package/src/core/glbStructure.ts +100 -0
  13. package/src/core/headlessBridge.ts +92 -0
  14. package/src/core/mapView.ts +195 -0
  15. package/src/core/math3d.ts +275 -0
  16. package/src/core/particleBridge.ts +154 -0
  17. package/src/core/physicsBridge.ts +305 -0
  18. package/src/core/recording.ts +59 -0
  19. package/src/core/sceneCore.ts +833 -0
  20. package/src/core/serialize.ts +144 -0
  21. package/src/gen/unsupported.gen.ts +212 -0
  22. package/src/index.ts +5 -0
  23. package/src/vendor/creator-anim/dist/creatorAnim.d.ts +123 -0
  24. package/src/vendor/creator-anim/dist/creatorAnim.js +14 -0
  25. package/src/vendor/creator-anim/js/index.ts +656 -0
  26. package/src/vendor/creator-nav/dist/creatorNav.d.ts +56 -0
  27. package/src/vendor/creator-nav/dist/creatorNav.js +14 -0
  28. package/src/vendor/creator-nav/js/index.ts +400 -0
  29. package/src/vendor/creator-particles/dist/creatorParticles.d.ts +43 -0
  30. package/src/vendor/creator-particles/dist/creatorParticles.js +14 -0
  31. package/src/vendor/creator-particles/js/index.ts +137 -0
  32. package/src/vendor/creator-physics/dist/creatorPhysics.d.ts +107 -0
  33. package/src/vendor/creator-physics/dist/creatorPhysics.js +14 -0
  34. package/src/vendor/creator-physics/js/index.ts +515 -0
  35. package/src/vite-env.d.ts +1 -0
  36. package/src/webgl/batching.ts +453 -0
  37. package/src/webgl/createViewerLite.ts +1627 -0
  38. package/src/webgl/customMaterial.ts +228 -0
  39. package/src/webgl/decoders/basis.ts +123 -0
  40. package/src/webgl/decoders/draco.ts +110 -0
  41. package/src/webgl/decoders/index.ts +68 -0
  42. package/src/webgl/decoders/worker.ts +45 -0
  43. package/src/webgl/editor.ts +1297 -0
  44. package/src/webgl/glb.ts +593 -0
  45. package/src/webgl/ibl.ts +137 -0
  46. package/src/webgl/shaders.ts +779 -0
@@ -0,0 +1,779 @@
1
+ // GLSL 300 es shaders for the lite 3D engine. The shading model follows Filament's published BRDF
2
+ // (GGX + Smith height-correlated visibility + Schlick fresnel, Lambert diffuse) with Filament's
3
+ // photometric units: light intensities arrive in lux (sun) / nits-ish (IBL) and are pre-exposed by
4
+ // the same default-camera exposure Filament uses (f/16, 1/125s, ISO 100), then tonemapped with an
5
+ // ACES fit and sRGB-encoded. That keeps the SDK's real-world intensity values (sun 100 000, IBL
6
+ // 20 000) producing the same overall brightness as the Filament build.
7
+
8
+ // Filament default camera: EV100 = log2(N^2 / t * 100/S) with N=16, t=1/125, S=100 → ~14.965.
9
+ // exposure = 1 / (1.2 * 2^EV100).
10
+ export const EXPOSURE = 1 / (1.2 * Math.pow(2, Math.log2((16 * 16) / (1 / 125))))
11
+
12
+ // ---- shared chunks ----------------------------------------------------------------------------
13
+
14
+ const COMMON_VERT_HEADER = /* glsl */ `#version 300 es
15
+ precision highp float;
16
+ in vec3 position;
17
+ in vec3 normal;
18
+ in vec2 uv;
19
+ uniform mat4 modelMatrix;
20
+ uniform mat4 modelViewMatrix;
21
+ uniform mat4 projectionMatrix;
22
+ uniform mat3 normalMatrix;
23
+ out vec3 vWorldPos;
24
+ out vec3 vNormal;
25
+ out vec2 vUv;
26
+ `
27
+
28
+ // Skinning chunk (same bone-texture layout as OGL's GLTFSkin).
29
+ const SKIN_CHUNK = /* glsl */ `
30
+ in vec4 skinIndex;
31
+ in vec4 skinWeight;
32
+ uniform sampler2D boneTexture;
33
+ uniform int boneTextureSize;
34
+
35
+ mat4 getBoneMatrix(const in float i) {
36
+ float j = i * 4.0;
37
+ float x = mod(j, float(boneTextureSize));
38
+ float y = floor(j / float(boneTextureSize));
39
+ float dx = 1.0 / float(boneTextureSize);
40
+ float dy = 1.0 / float(boneTextureSize);
41
+ y = dy * (y + 0.5);
42
+ vec4 v1 = texture(boneTexture, vec2(dx * (x + 0.5), y));
43
+ vec4 v2 = texture(boneTexture, vec2(dx * (x + 1.5), y));
44
+ vec4 v3 = texture(boneTexture, vec2(dx * (x + 2.5), y));
45
+ vec4 v4 = texture(boneTexture, vec2(dx * (x + 3.5), y));
46
+ return mat4(v1, v2, v3, v4);
47
+ }
48
+
49
+ mat4 getSkinMatrix() {
50
+ mat4 boneMatX = getBoneMatrix(skinIndex.x);
51
+ mat4 boneMatY = getBoneMatrix(skinIndex.y);
52
+ mat4 boneMatZ = getBoneMatrix(skinIndex.z);
53
+ mat4 boneMatW = getBoneMatrix(skinIndex.w);
54
+ return boneMatX * skinWeight.x + boneMatY * skinWeight.y + boneMatZ * skinWeight.z + boneMatW * skinWeight.w;
55
+ }
56
+ `
57
+
58
+ // Morph-target (blendshape) chunk — the three.js r133+ approach: every target's deltas live in one
59
+ // RGBA32F data texture fetched by gl_VertexID, so there is no vertex-attribute limit. Layout is
60
+ // target-major: texel = targetIndex * vertexCount + vertexId; normal-delta blocks (when present)
61
+ // follow all position blocks at offset count * vertexCount.
62
+ const MORPH_CHUNK = /* glsl */ `
63
+ uniform sampler2D uMorphTex;
64
+ uniform int uMorphCount; // active target count (capped by glb.ts)
65
+ uniform int uMorphVertexCount;
66
+ uniform int uMorphTexWidth;
67
+ uniform float uMorphWeights[8];
68
+
69
+ vec3 morphFetch(int idx) {
70
+ return texelFetch(uMorphTex, ivec2(idx % uMorphTexWidth, idx / uMorphTexWidth), 0).rgb;
71
+ }
72
+ `
73
+
74
+ // INSTANCED (batching.ts defines it on a program's twin): the world matrix is the per-instance
75
+ // `instanceMatrix` attribute and the view comes in separately — modelMatrix / modelViewMatrix /
76
+ // normalMatrix go inactive. Same math either way, so a batch and a single can't shade differently.
77
+ const vertex = (defines: string): string => /* glsl */ `${COMMON_VERT_HEADER}
78
+ #ifdef INSTANCED
79
+ in mat4 instanceMatrix;
80
+ uniform mat4 viewMatrix;
81
+ #endif
82
+ ${defines.includes('HAS_SKIN') ? SKIN_CHUNK : ''}
83
+ ${defines.includes('HAS_MORPH') ? MORPH_CHUNK : ''}
84
+ ${defines.includes('HAS_COLOR') ? 'in vec4 color;\nout vec4 vColor;' : ''}
85
+ uniform mat4 shadowMatrix;
86
+ out vec4 vShadowCoord;
87
+
88
+ void main() {
89
+ vec4 pos = vec4(position, 1.0);
90
+ vec3 nrm = normal;
91
+ ${defines.includes('HAS_MORPH') ? /* glsl — morph BEFORE skinning, per glTF spec */ `
92
+ for (int t = 0; t < uMorphCount; t++) {
93
+ if (uMorphWeights[t] != 0.0) {
94
+ pos.xyz += uMorphWeights[t] * morphFetch(t * uMorphVertexCount + gl_VertexID);
95
+ ${defines.includes('HAS_MORPH_NORMALS') ? 'nrm += uMorphWeights[t] * morphFetch((uMorphCount + t) * uMorphVertexCount + gl_VertexID);' : ''}
96
+ }
97
+ }
98
+ ` : ''}
99
+ ${defines.includes('HAS_SKIN') ? `
100
+ mat4 skinMatrix = getSkinMatrix();
101
+ pos = skinMatrix * pos;
102
+ nrm = mat3(skinMatrix) * nrm;
103
+ ` : ''}
104
+ ${defines.includes('HAS_COLOR') ? 'vColor = color;' : ''}
105
+ #ifdef INSTANCED
106
+ mat4 model = instanceMatrix;
107
+ #else
108
+ mat4 model = modelMatrix;
109
+ #endif
110
+ vec4 worldPos = model * pos;
111
+ vWorldPos = worldPos.xyz;
112
+ vNormal = normalize(mat3(model) * nrm);
113
+ vUv = uv;
114
+ vShadowCoord = shadowMatrix * worldPos;
115
+ #ifdef INSTANCED
116
+ gl_Position = projectionMatrix * viewMatrix * worldPos;
117
+ #else
118
+ gl_Position = projectionMatrix * modelViewMatrix * pos;
119
+ #endif
120
+ }
121
+ `
122
+
123
+ // Fragment lighting shared by lit + glTF PBR + custom lit materials (customMaterial.ts).
124
+ // Requires uniforms:
125
+ // sunDirection (unit, TO the scene), sunColorIntensity (linear rgb * lux), iblLuminance (intensity),
126
+ // iblSH[9], envMap (prefiltered cube, 4 mips), shadowMap, shadowEnabled, exposure, cameraPosition.
127
+ export const LIGHTING_CHUNK = /* glsl */ `
128
+ uniform vec3 sunDirection;
129
+ uniform vec4 sunColorIntensity; // rgb = linear color, a = intensity (lux)
130
+ uniform float iblLuminance;
131
+ uniform vec3 iblSH[9];
132
+ uniform samplerCube envMap;
133
+ uniform float envMapMaxLod;
134
+ uniform sampler2D shadowMap;
135
+ uniform float shadowEnabled;
136
+ uniform float shadowMapSize;
137
+ uniform float exposure;
138
+ in vec4 vShadowCoord;
139
+
140
+ #define PI 3.14159265359
141
+
142
+ float D_GGX(float NoH, float a) {
143
+ float a2 = a * a;
144
+ float f = (NoH * a2 - NoH) * NoH + 1.0;
145
+ return a2 / (PI * f * f);
146
+ }
147
+
148
+ float V_SmithGGXCorrelated(float NoV, float NoL, float a) {
149
+ float a2 = a * a;
150
+ float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
151
+ float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
152
+ return 0.5 / (GGXV + GGXL + 1e-5);
153
+ }
154
+
155
+ vec3 F_Schlick(float u, vec3 f0) {
156
+ float f = pow(1.0 - u, 5.0);
157
+ return f0 + (vec3(1.0) - f0) * f;
158
+ }
159
+
160
+ vec3 evalSH(vec3 n) {
161
+ return max(
162
+ iblSH[0]
163
+ + iblSH[1] * n.y
164
+ + iblSH[2] * n.z
165
+ + iblSH[3] * n.x
166
+ + iblSH[4] * (n.y * n.x)
167
+ + iblSH[5] * (n.y * n.z)
168
+ + iblSH[6] * (3.0 * n.z * n.z - 1.0)
169
+ + iblSH[7] * (n.z * n.x)
170
+ + iblSH[8] * (n.x * n.x - n.y * n.y)
171
+ , vec3(0.0));
172
+ }
173
+
174
+ float sampleShadowPCF() {
175
+ if (shadowEnabled < 0.5) return 1.0;
176
+ vec3 coord = vShadowCoord.xyz / vShadowCoord.w;
177
+ coord = coord * 0.5 + 0.5;
178
+ if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0 || coord.z > 1.0) return 1.0;
179
+ float bias = 0.0018;
180
+ float texel = 1.0 / shadowMapSize;
181
+ float sum = 0.0;
182
+ for (int x = -1; x <= 1; x++) {
183
+ for (int y = -1; y <= 1; y++) {
184
+ float depth = texture(shadowMap, coord.xy + vec2(float(x), float(y)) * texel).r;
185
+ sum += (coord.z - bias) > depth ? 0.0 : 1.0;
186
+ }
187
+ }
188
+ return sum / 9.0;
189
+ }
190
+
191
+ // diffuseColor: linear albedo (already metallic-adjusted); f0: specular reflectance at normal.
192
+ vec3 shadePBR(vec3 n, vec3 v, vec3 diffuseColor, vec3 f0, float perceptualRoughness, float occlusion) {
193
+ float roughness = clamp(perceptualRoughness, 0.045, 1.0);
194
+ float a = roughness * roughness;
195
+ float NoV = abs(dot(n, v)) + 1e-5;
196
+
197
+ vec3 color = vec3(0.0);
198
+
199
+ // --- sun ---
200
+ if (sunColorIntensity.a > 0.0) {
201
+ vec3 l = normalize(-sunDirection);
202
+ float NoL = clamp(dot(n, l), 0.0, 1.0);
203
+ if (NoL > 0.0) {
204
+ vec3 h = normalize(v + l);
205
+ float NoH = clamp(dot(n, h), 0.0, 1.0);
206
+ float LoH = clamp(dot(l, h), 0.0, 1.0);
207
+ float D = D_GGX(NoH, a);
208
+ float V = V_SmithGGXCorrelated(NoV, NoL, a);
209
+ vec3 F = F_Schlick(LoH, f0);
210
+ vec3 Fr = D * V * F;
211
+ vec3 Fd = diffuseColor * (1.0 / PI);
212
+ float shadow = sampleShadowPCF();
213
+ // "illuminance" model: lux * NoL, pre-exposed
214
+ color += (Fd + Fr) * sunColorIntensity.rgb * (sunColorIntensity.a * exposure * NoL * shadow);
215
+ }
216
+ }
217
+
218
+ // --- IBL ---
219
+ if (iblLuminance > 0.0) {
220
+ // cmgen's SH coefficients are pre-convolved irradiance — no extra 1/PI (matches Filament).
221
+ vec3 irradiance = evalSH(n);
222
+ vec3 Fd = diffuseColor * irradiance;
223
+
224
+ vec3 r = reflect(-v, n);
225
+ float lod = roughness * envMapMaxLod;
226
+ vec3 prefiltered = textureLod(envMap, r, lod).rgb;
227
+ // Simple DFG approximation (Karis): avoids a DFG LUT.
228
+ vec3 specular = prefiltered * (f0 * (1.0 - roughness) + roughness * 0.25 * f0 + 0.05 * (1.0 - roughness));
229
+ color += (Fd + specular) * (iblLuminance * exposure * occlusion);
230
+ }
231
+
232
+ return color;
233
+ }
234
+
235
+ // ACES filmic fit (Narkowicz) + sRGB OETF — stands in for Filament's default ACES color grading.
236
+ vec3 tonemap(vec3 x) {
237
+ const float A = 2.51; const float B = 0.03; const float C = 2.43; const float D = 0.59; const float E = 0.14;
238
+ vec3 t = clamp((x * (A * x + B)) / (x * (C * x + D) + E), 0.0, 1.0);
239
+ return pow(t, vec3(1.0 / 2.2));
240
+ }
241
+ `
242
+
243
+ // ---- programs ---------------------------------------------------------------------------------
244
+
245
+ export const litFragment = /* glsl */ `#version 300 es
246
+ precision highp float;
247
+ in vec3 vWorldPos;
248
+ in vec3 vNormal;
249
+ in vec2 vUv;
250
+ uniform vec3 cameraPosition;
251
+ uniform vec3 baseColor; // linear factor over baseColorMap (both default white)
252
+ uniform sampler2D baseColorMap;
253
+ uniform float roughness;
254
+ uniform float metallic;
255
+ ${LIGHTING_CHUNK}
256
+ out vec4 fragColor;
257
+
258
+ void main() {
259
+ vec3 albedo = baseColor * texture(baseColorMap, vUv).rgb;
260
+ vec3 n = normalize(vNormal);
261
+ vec3 v = normalize(cameraPosition - vWorldPos);
262
+ if (dot(n, v) < 0.0) n = -n;
263
+ vec3 diffuseColor = albedo * (1.0 - metallic);
264
+ vec3 f0 = mix(vec3(0.04), albedo, metallic);
265
+ vec3 color = shadePBR(n, v, diffuseColor, f0, roughness, 1.0);
266
+ fragColor = vec4(tonemap(color), 1.0);
267
+ }
268
+ `
269
+
270
+ // terrain.mat (docs/terrain-plan.md §1.5): four albedo layers + two packed normal-map textures
271
+ // blended by a control map on the terrain's own grid; UV = the chunk's local XZ in metres (the SDK's
272
+ // terrainMesh.ts writes it), so `tiling` is metres per repeat and the control map is addressed as
273
+ // (uv / cellSize + 0.5) / gridSize. The tangent frame comes from screen-space derivatives (the lite
274
+ // chunk geometry carries no tangents). No lightmap block here — lite has no bake.
275
+ export const terrainFragment = /* glsl */ `#version 300 es
276
+ precision highp float;
277
+ in vec3 vWorldPos;
278
+ in vec3 vNormal;
279
+ in vec2 vUv;
280
+ uniform vec3 cameraPosition;
281
+ uniform sampler2D control;
282
+ uniform sampler2D albedo0;
283
+ uniform sampler2D albedo1;
284
+ uniform sampler2D albedo2;
285
+ uniform sampler2D albedo3;
286
+ uniform sampler2D normals01;
287
+ uniform sampler2D normals23;
288
+ uniform vec4 tiling;
289
+ uniform vec4 roughness;
290
+ uniform vec4 normalScale;
291
+ uniform vec4 triplanar;
292
+ uniform vec2 gridSize;
293
+ uniform float cellSize;
294
+ uniform vec3 tint;
295
+ ${LIGHTING_CHUNK}
296
+ out vec4 fragColor;
297
+
298
+ vec4 triplanarSample(sampler2D tex, vec3 p, vec3 bw, float s) {
299
+ return texture(tex, p.zy / s) * bw.x + texture(tex, p.xz / s) * bw.y + texture(tex, p.xy / s) * bw.z;
300
+ }
301
+
302
+ void splatLayer(sampler2D alb, sampler2D pack, float slot, float tl, float nscale, float tri, float w,
303
+ vec2 uv, vec3 wp, vec3 bw, inout vec3 albedo, inout vec3 nrm) {
304
+ if (w <= 0.0) return;
305
+ float s = max(tl, 1e-3);
306
+ if (tri > 0.5) {
307
+ albedo += triplanarSample(alb, wp, bw, s).rgb * w;
308
+ nrm += vec3(0.0, 0.0, 1.0) * w;
309
+ } else {
310
+ vec2 luv = uv / s;
311
+ albedo += texture(alb, luv).rgb * w;
312
+ if (nscale > 0.0) {
313
+ vec4 p = texture(pack, luv);
314
+ vec2 xy = (slot > 0.5 ? p.ba : p.rg) * 2.0 - 1.0;
315
+ xy *= nscale;
316
+ nrm += vec3(xy, sqrt(max(1.0 - dot(xy, xy), 0.0))) * w;
317
+ } else {
318
+ nrm += vec3(0.0, 0.0, 1.0) * w;
319
+ }
320
+ }
321
+ }
322
+
323
+ vec3 applyTangentNormal(vec3 nTangent, vec3 ng) {
324
+ vec3 pos_dx = dFdx(vWorldPos);
325
+ vec3 pos_dy = dFdy(vWorldPos);
326
+ vec2 tex_dx = dFdx(vUv);
327
+ vec2 tex_dy = dFdy(vUv);
328
+ float det = tex_dx.x * tex_dy.y - tex_dy.x * tex_dx.y;
329
+ if (abs(det) < 1e-8) return ng;
330
+ vec3 t = normalize(pos_dx * tex_dy.y - pos_dy * tex_dx.y);
331
+ t = normalize(t - ng * dot(ng, t));
332
+ vec3 b = normalize(cross(ng, t)) * sign(det);
333
+ return normalize(mat3(t, b, ng) * nTangent);
334
+ }
335
+
336
+ void main() {
337
+ vec2 cuv = (vUv / max(cellSize, 1e-4) + 0.5) / max(gridSize, vec2(1.0));
338
+ vec4 w = texture(control, cuv);
339
+ float ws = w.r + w.g + w.b + w.a;
340
+ w = ws > 1e-4 ? w / ws : vec4(1.0, 0.0, 0.0, 0.0);
341
+ vec3 ng = normalize(vNormal);
342
+ vec3 bw = abs(ng);
343
+ bw = bw / max(bw.x + bw.y + bw.z, 1e-4);
344
+ vec3 albedo = vec3(0.0);
345
+ vec3 nrm = vec3(0.0);
346
+ splatLayer(albedo0, normals01, 0.0, tiling.x, normalScale.x, triplanar.x, w.r, vUv, vWorldPos, bw, albedo, nrm);
347
+ splatLayer(albedo1, normals01, 1.0, tiling.y, normalScale.y, triplanar.y, w.g, vUv, vWorldPos, bw, albedo, nrm);
348
+ splatLayer(albedo2, normals23, 0.0, tiling.z, normalScale.z, triplanar.z, w.b, vUv, vWorldPos, bw, albedo, nrm);
349
+ splatLayer(albedo3, normals23, 1.0, tiling.w, normalScale.w, triplanar.w, w.a, vUv, vWorldPos, bw, albedo, nrm);
350
+ vec3 n = applyTangentNormal(normalize(nrm), ng);
351
+ vec3 v = normalize(cameraPosition - vWorldPos);
352
+ if (dot(ng, v) < 0.0) n = -n;
353
+ vec3 diffuseColor = albedo * tint;
354
+ vec3 color = shadePBR(n, v, diffuseColor, vec3(0.04), dot(roughness, w), 1.0);
355
+ fragColor = vec4(tonemap(color), 1.0);
356
+ }
357
+ `
358
+
359
+ // The twin of unlit.mat: factor x map, both default white. OPAQUE — alpha plays no part (Filament's
360
+ // unlit.mat declares baseColor as a float3 and ignores the map's alpha), so the fragment forces 1.
361
+ export const unlitFragment = /* glsl */ `#version 300 es
362
+ precision highp float;
363
+ in vec3 vWorldPos;
364
+ in vec3 vNormal;
365
+ in vec2 vUv;
366
+ uniform vec4 baseColor; // linear, premultiplied; factor over baseColorMap
367
+ uniform sampler2D baseColorMap;
368
+ out vec4 fragColor;
369
+
370
+ vec3 tonemapUnlit(vec3 x) {
371
+ const float A = 2.51; const float B = 0.03; const float C = 2.43; const float D = 0.59; const float E = 0.14;
372
+ vec3 t = clamp((x * (A * x + B)) / (x * (C * x + D) + E), 0.0, 1.0);
373
+ return pow(t, vec3(1.0 / 2.2));
374
+ }
375
+
376
+ void main() {
377
+ vec4 c = baseColor * texture(baseColorMap, vUv);
378
+ fragColor = vec4(tonemapUnlit(c.rgb), 1.0);
379
+ }
380
+ `
381
+
382
+ // The twin of unlit-transparent.mat: same maths on a float4 colour, PREMULTIPLIED at the end because
383
+ // the blend func is (ONE, ONE_MINUS_SRC_ALPHA) — the straight-alpha inputs (a decoded PNG, and a
384
+ // colour that PREMULTIPLIED_sRGB only converts without multiplying) are premultiplied exactly once,
385
+ // after tonemapping. Getting this wrong is the classic bright halo around a faded sprite.
386
+ export const unlitBlendFragment = /* glsl */ `#version 300 es
387
+ precision highp float;
388
+ in vec3 vWorldPos;
389
+ in vec3 vNormal;
390
+ in vec2 vUv;
391
+ uniform vec4 baseColor; // linear, straight alpha; factor over baseColorMap
392
+ uniform sampler2D baseColorMap;
393
+ out vec4 fragColor;
394
+
395
+ vec3 tonemapUnlit(vec3 x) {
396
+ const float A = 2.51; const float B = 0.03; const float C = 2.43; const float D = 0.59; const float E = 0.14;
397
+ vec3 t = clamp((x * (A * x + B)) / (x * (C * x + D) + E), 0.0, 1.0);
398
+ return pow(t, vec3(1.0 / 2.2));
399
+ }
400
+
401
+ void main() {
402
+ vec4 c = baseColor * texture(baseColorMap, vUv);
403
+ fragColor = vec4(tonemapUnlit(c.rgb) * c.a, c.a);
404
+ }
405
+ `
406
+
407
+ // video.mat: unlit external sample, inverse-tonemapped by Filament so the pipeline round-trips.
408
+ // Here we just pass the (sRGB-encoded) video texel through untouched.
409
+ export const videoFragment = /* glsl */ `#version 300 es
410
+ precision highp float;
411
+ in vec2 vUv;
412
+ in vec3 vWorldPos;
413
+ in vec3 vNormal;
414
+ uniform sampler2D baseColorMap;
415
+ out vec4 fragColor;
416
+ void main() {
417
+ fragColor = vec4(texture(baseColorMap, vUv).rgb, 1.0);
418
+ }
419
+ `
420
+
421
+ // shadow.mat: invisible shadow-catcher plane (shadowMultiplier) — opacity = color.a where shadowed.
422
+ export const shadowCatcherFragment = /* glsl */ `#version 300 es
423
+ precision highp float;
424
+ in vec3 vWorldPos;
425
+ in vec3 vNormal;
426
+ in vec2 vUv;
427
+ uniform vec4 color; // linear, premultiplied
428
+ ${LIGHTING_CHUNK}
429
+ out vec4 fragColor;
430
+ void main() {
431
+ float vis = sampleShadowPCF();
432
+ float shadowAmount = 1.0 - vis;
433
+ fragColor = vec4(color.rgb * shadowAmount, color.a * shadowAmount);
434
+ }
435
+ `
436
+
437
+ // glTF PBR (metallic-roughness) — configured with #define flags per material.
438
+ export const gltfVertex = (defines: string): string => vertex(defines)
439
+ export const litVertex = vertex('')
440
+
441
+ export const gltfFragment = (defines: string): string => /* glsl */ `#version 300 es
442
+ precision highp float;
443
+ ${defines}
444
+ in vec3 vWorldPos;
445
+ in vec3 vNormal;
446
+ in vec2 vUv;
447
+ #ifdef HAS_COLOR
448
+ in vec4 vColor;
449
+ #endif
450
+ uniform vec3 cameraPosition;
451
+ uniform vec4 uBaseColorFactor;
452
+ uniform float uMetallic;
453
+ uniform float uRoughness;
454
+ uniform vec3 uEmissive;
455
+ uniform float uAlphaCutoff;
456
+ #ifdef HAS_BASECOLOR_MAP
457
+ uniform sampler2D uBaseColorMap;
458
+ #endif
459
+ #ifdef HAS_MR_MAP
460
+ uniform sampler2D uMRMap;
461
+ #endif
462
+ #ifdef HAS_NORMAL_MAP
463
+ uniform sampler2D uNormalMap;
464
+ #endif
465
+ #ifdef HAS_OCCLUSION_MAP
466
+ uniform sampler2D uOcclusionMap;
467
+ #endif
468
+ #ifdef HAS_EMISSIVE_MAP
469
+ uniform sampler2D uEmissiveMap;
470
+ #endif
471
+ ${LIGHTING_CHUNK}
472
+ out vec4 fragColor;
473
+
474
+ #ifdef HAS_NORMAL_MAP
475
+ vec3 getNormal() {
476
+ vec3 pos_dx = dFdx(vWorldPos);
477
+ vec3 pos_dy = dFdy(vWorldPos);
478
+ vec2 tex_dx = dFdx(vUv);
479
+ vec2 tex_dy = dFdy(vUv);
480
+ float det = tex_dx.x * tex_dy.y - tex_dy.x * tex_dx.y;
481
+ if (abs(det) < 1e-8) return normalize(vNormal);
482
+ vec3 t = normalize(pos_dx * tex_dy.y - pos_dy * tex_dx.y);
483
+ vec3 ng = normalize(vNormal);
484
+ t = normalize(t - ng * dot(ng, t));
485
+ vec3 b = normalize(cross(ng, t)) * sign(det);
486
+ mat3 tbn = mat3(t, b, ng);
487
+ vec3 n = texture(uNormalMap, vUv).rgb * 2.0 - 1.0;
488
+ return normalize(tbn * n);
489
+ }
490
+ #endif
491
+
492
+ void main() {
493
+ vec4 base = uBaseColorFactor;
494
+ #ifdef HAS_BASECOLOR_MAP
495
+ base *= texture(uBaseColorMap, vUv);
496
+ #endif
497
+ #ifdef HAS_COLOR
498
+ base *= vColor;
499
+ #endif
500
+ #ifdef ALPHA_MASK
501
+ if (base.a < uAlphaCutoff) discard;
502
+ #endif
503
+
504
+ #ifdef UNLIT
505
+ // KHR_materials_unlit: the base colour IS the shaded colour — no lights, no IBL, no exposure
506
+ // (Filament's UNLIT shading model does the same, then tone-maps in post like everything else).
507
+ #ifdef ALPHA_BLEND
508
+ fragColor = vec4(tonemap(base.rgb), base.a);
509
+ #else
510
+ fragColor = vec4(tonemap(base.rgb), 1.0);
511
+ #endif
512
+ return;
513
+ #endif
514
+
515
+ float metallic = uMetallic;
516
+ float roughness = uRoughness;
517
+ #ifdef HAS_MR_MAP
518
+ vec3 mr = texture(uMRMap, vUv).rgb;
519
+ roughness *= mr.g;
520
+ metallic *= mr.b;
521
+ #endif
522
+
523
+ float occlusion = 1.0;
524
+ #ifdef HAS_OCCLUSION_MAP
525
+ occlusion = texture(uOcclusionMap, vUv).r;
526
+ #endif
527
+
528
+ #ifdef HAS_NORMAL_MAP
529
+ vec3 n = getNormal();
530
+ #else
531
+ vec3 n = normalize(vNormal);
532
+ #endif
533
+ vec3 v = normalize(cameraPosition - vWorldPos);
534
+ #ifdef DOUBLE_SIDED
535
+ if (dot(n, v) < 0.0) n = -n;
536
+ #endif
537
+
538
+ vec3 diffuseColor = base.rgb * (1.0 - metallic);
539
+ vec3 f0 = mix(vec3(0.04), base.rgb, metallic);
540
+ vec3 color = shadePBR(n, v, diffuseColor, f0, roughness, occlusion);
541
+
542
+ vec3 emissive = uEmissive;
543
+ #ifdef HAS_EMISSIVE_MAP
544
+ emissive *= texture(uEmissiveMap, vUv).rgb;
545
+ #endif
546
+ color += emissive * iblLuminance * exposure;
547
+
548
+ #ifdef ALPHA_BLEND
549
+ fragColor = vec4(tonemap(color), base.a);
550
+ #else
551
+ fragColor = vec4(tonemap(color), 1.0);
552
+ #endif
553
+ }
554
+ `
555
+
556
+ // ---- wireframe (editor viewport modes) --------------------------------------------------------
557
+ // Solid-color fragment for the LINES companion meshes (createViewerLite _setWireframeMode). Reuses
558
+ // the standard vertex shaders (litVertex / gltfVertex('HAS_SKIN')) so skinned wires pose correctly;
559
+ // the extra varyings they emit are simply unused here.
560
+ export const wireFragment = /* glsl */ `#version 300 es
561
+ precision highp float;
562
+ in vec3 vWorldPos;
563
+ in vec3 vNormal;
564
+ in vec2 vUv;
565
+ uniform vec3 uWireColor;
566
+ out vec4 fragColor;
567
+ void main() { fragColor = vec4(uWireColor, 1.0); }
568
+ `
569
+
570
+ // ---- shadow (depth) pass ----------------------------------------------------------------------
571
+
572
+ /** Depth-only vertex stage (shadow map, selection mask). `instanced` reads the world matrix from
573
+ * the batch stream instead of the modelMatrix uniform (batching.ts). */
574
+ export const depthVertex = (skin: boolean, instanced = false): string => /* glsl */ `#version 300 es
575
+ precision highp float;
576
+ in vec3 position;
577
+ ${skin ? 'in vec3 normal;\nin vec2 uv;' : ''}
578
+ ${instanced ? 'in mat4 instanceMatrix;' : 'uniform mat4 modelMatrix;'}
579
+ uniform mat4 shadowViewProjection;
580
+ ${skin ? SKIN_CHUNK : ''}
581
+ void main() {
582
+ vec4 pos = vec4(position, 1.0);
583
+ ${skin ? 'pos = getSkinMatrix() * pos;' : ''}
584
+ gl_Position = shadowViewProjection * ${instanced ? 'instanceMatrix' : 'modelMatrix'} * pos;
585
+ }
586
+ `
587
+
588
+ export const depthFragment = /* glsl */ `#version 300 es
589
+ precision highp float;
590
+ out vec4 fragColor;
591
+ void main() { fragColor = vec4(1.0); }
592
+ `
593
+
594
+ // ---- color helpers ----------------------------------------------------------------------------
595
+
596
+ export const srgbToLinear = (c: number): number =>
597
+ c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
598
+
599
+ /** JS-side twin of the shader tonemap — for values written outside a shader (the clear color). */
600
+ export const tonemapJs = (x: number): number => {
601
+ const t = Math.min(1, Math.max(0, (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14)))
602
+ return Math.pow(t, 1 / 2.2)
603
+ }
604
+
605
+ /** 0xRRGGBB → linear [r,g,b] (setUniformRgb / RgbType::sRGB). */
606
+ export const hexToLinearRgb = (hex: number): [number, number, number] => [
607
+ srgbToLinear(((hex >>> 16) & 0xff) / 255),
608
+ srgbToLinear(((hex >>> 8) & 0xff) / 255),
609
+ srgbToLinear((hex & 0xff) / 255),
610
+ ]
611
+
612
+ /** 0xRRGGBBAA → linear premultiplied [r,g,b,a] (setUniformRgba / RgbaType::PREMULTIPLIED_sRGB). */
613
+ export const hexToLinearRgba = (hex: number): [number, number, number, number] => [
614
+ srgbToLinear(((hex >>> 24) & 0xff) / 255),
615
+ srgbToLinear(((hex >>> 16) & 0xff) / 255),
616
+ srgbToLinear(((hex >>> 8) & 0xff) / 255),
617
+ (hex & 0xff) / 255,
618
+ ]
619
+
620
+ // ---- particles (creator-particles streams; twins of creator-gl/materials/src/particles*.mat) ----
621
+ // The stream carries per-vertex color + custom (size, rotation, opacity, frame); the look (flipbook
622
+ // cells, softness, emission, additive, billboarding / stretch / ribbon) is the shader, driven by the
623
+ // same uniforms the Filament materials declare. Output is premultiplied, tonemapped like unlit.
624
+ // Not here: soft particles (depthFade) — the lite pipeline has no depth prepass to read.
625
+
626
+ const PARTICLE_FRAG_COMMON = /* glsl */ `
627
+ uniform sampler2D baseColorMap;
628
+ uniform vec2 sheetSize; // (cols, rows) of the flipbook sheet, both >= 1 (the SDK primes it)
629
+ uniform float emissive;
630
+ uniform float additive;
631
+ uniform float softness;
632
+ uniform float depthFade; // declared so the SDK's write lands somewhere; ignored (no depth buffer)
633
+
634
+ vec3 tonemapParticle(vec3 x) {
635
+ const float A = 2.51; const float B = 0.03; const float C = 2.43; const float D = 0.59; const float E = 0.14;
636
+ vec3 t = clamp((x * (A * x + B)) / (x * (C * x + D) + E), 0.0, 1.0);
637
+ return pow(t, vec3(1.0 / 2.2));
638
+ }
639
+ // Integer cell math: a float floor(frame / cols) lands one row early at row starts on the GPU.
640
+ vec2 sheetCell(int fi) {
641
+ int sx = int(sheetSize.x + 0.5);
642
+ return vec2(float(fi - (fi / sx) * sx), float(fi / sx));
643
+ }
644
+ `
645
+
646
+ export const particlesPointVertex = /* glsl */ `#version 300 es
647
+ precision highp float;
648
+ in vec3 position;
649
+ in vec4 color;
650
+ in vec4 custom;
651
+ uniform mat4 modelMatrix;
652
+ uniform mat4 viewMatrix;
653
+ uniform mat4 projectionMatrix;
654
+ uniform float perspScale; // viewportHeightPx / (2 tan(fov/2)): a true world-space diameter
655
+ out vec4 vColor;
656
+ out vec4 vCustom;
657
+ void main() {
658
+ vec4 viewPos = viewMatrix * modelMatrix * vec4(position, 1.0);
659
+ float z = max(-viewPos.z, 1e-3);
660
+ gl_PointSize = custom.x * perspScale / z;
661
+ vColor = color;
662
+ vCustom = custom;
663
+ gl_Position = projectionMatrix * viewPos;
664
+ }
665
+ `
666
+
667
+ export const particlesPointFragment = /* glsl */ `#version 300 es
668
+ precision highp float;
669
+ in vec4 vColor;
670
+ in vec4 vCustom;
671
+ ${PARTICLE_FRAG_COMMON}
672
+ out vec4 fragColor;
673
+ void main() {
674
+ vec2 uv = gl_PointCoord.xy;
675
+ float rot = vCustom.y;
676
+ if (rot != 0.0) {
677
+ float co = cos(rot);
678
+ float si = sin(rot);
679
+ vec2 p = uv - vec2(0.5);
680
+ uv = vec2(co * p.x - si * p.y, si * p.x + co * p.y) + vec2(0.5);
681
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) discard;
682
+ }
683
+ int total = int(sheetSize.x + 0.5) * int(sheetSize.y + 0.5);
684
+ int fi = int(floor(vCustom.w) + 0.5);
685
+ fi = fi - (fi / total) * total;
686
+ vec4 tex = texture(baseColorMap, (sheetCell(fi) + uv) / sheetSize);
687
+ float radial = 1.0 - smoothstep(0.0, 0.5, length(gl_PointCoord.xy - vec2(0.5)));
688
+ float shape = mix(1.0, radial, clamp(softness, 0.0, 1.0));
689
+ float alpha = tex.a * vColor.a * vCustom.z * shape;
690
+ vec3 rgb = tex.rgb * vColor.rgb * (1.0 + emissive);
691
+ fragColor = vec4(tonemapParticle(rgb) * alpha, alpha * (1.0 - clamp(additive, 0.0, 1.0)));
692
+ }
693
+ `
694
+
695
+ export const particlesQuadVertex = /* glsl */ `#version 300 es
696
+ precision highp float;
697
+ in vec3 position;
698
+ in vec4 color;
699
+ in vec4 custom; // size, rotation, opacity, frame
700
+ in vec3 aux; // quad: velocity (stretch axis); ribbon: strip tangent — sim space
701
+ in vec2 corner; // quad: ±0.5 square; ribbon: (u01 - 0.5, ±0.5 side)
702
+ uniform mat4 modelMatrix;
703
+ uniform mat4 viewMatrix;
704
+ uniform mat4 projectionMatrix;
705
+ uniform float mode; // 0 camera billboard, 1 velocity-stretched, 2 ribbon strip
706
+ uniform float stretch; // (mode 1) extra length per unit of speed: length = size + stretch · |v|
707
+ out vec4 vColor;
708
+ out vec4 vCustom;
709
+ out vec4 vQuadUv; // xy: in-cell uv, zw: the softness basis
710
+ void main() {
711
+ vec4 wp = modelMatrix * vec4(position, 1.0);
712
+ vec3 a = mat3(modelMatrix) * aux;
713
+ // the camera's world axes: rows of the view rotation (= columns of its inverse)
714
+ vec3 camX = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
715
+ vec3 camY = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
716
+ vec3 camZ = vec3(viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2]); // toward the camera
717
+ vec3 offset;
718
+ vec2 suv = corner;
719
+ if (mode < 0.5) {
720
+ float co = cos(custom.y);
721
+ float si = sin(custom.y);
722
+ vec2 rc = vec2(co * corner.x - si * corner.y, si * corner.x + co * corner.y);
723
+ offset = (camX * rc.x + camY * rc.y) * custom.x;
724
+ } else if (mode < 1.5) {
725
+ float speed = length(a);
726
+ vec3 dir = speed > 1e-4 ? a / speed : camY;
727
+ vec3 y = dir - camZ * dot(dir, camZ);
728
+ float yl = length(y);
729
+ y = yl > 1e-4 ? y / yl : camY;
730
+ vec3 x = normalize(cross(y, camZ));
731
+ offset = x * corner.x * custom.x + y * corner.y * (custom.x + stretch * speed * yl);
732
+ } else {
733
+ vec3 x = cross(a, camZ);
734
+ float xl = length(x);
735
+ x = xl > 1e-4 ? x / xl : camX;
736
+ offset = x * corner.y * custom.x;
737
+ suv = vec2(0.0, corner.y);
738
+ }
739
+ wp.xyz += offset;
740
+ vColor = color;
741
+ vCustom = custom;
742
+ vQuadUv = vec4(corner.x + 0.5, 0.5 - corner.y, suv.x, suv.y);
743
+ gl_Position = projectionMatrix * viewMatrix * wp;
744
+ }
745
+ `
746
+
747
+ export const particlesQuadFragment = /* glsl */ `#version 300 es
748
+ precision highp float;
749
+ in vec4 vColor;
750
+ in vec4 vCustom;
751
+ in vec4 vQuadUv;
752
+ ${PARTICLE_FRAG_COMMON}
753
+ uniform sampler2D emissiveMap;
754
+ uniform vec3 emissiveColor;
755
+ uniform float frameBlend;
756
+ out vec4 fragColor;
757
+ void main() {
758
+ vec2 uv = vQuadUv.xy;
759
+ float total = sheetSize.x * sheetSize.y;
760
+ float fidx = mod(vCustom.w, total);
761
+ float frame = floor(fidx);
762
+ int fi = int(frame + 0.5);
763
+ vec2 tuv = (sheetCell(fi) + uv) / sheetSize;
764
+ vec4 tex = texture(baseColorMap, tuv);
765
+ vec3 em = texture(emissiveMap, tuv).rgb;
766
+ if (frameBlend > 0.5) {
767
+ int ni = min(fi + 1, int(total + 0.5) - 1);
768
+ vec2 tuv2 = (sheetCell(ni) + uv) / sheetSize;
769
+ float f = fidx - frame;
770
+ tex = mix(tex, texture(baseColorMap, tuv2), f);
771
+ em = mix(em, texture(emissiveMap, tuv2).rgb, f);
772
+ }
773
+ float radial = 1.0 - smoothstep(0.0, 0.5, length(vQuadUv.zw));
774
+ float shape = mix(1.0, radial, clamp(softness, 0.0, 1.0));
775
+ float alpha = tex.a * vColor.a * vCustom.z * shape;
776
+ vec3 rgb = tex.rgb * vColor.rgb * (1.0 + emissive) + em * emissiveColor;
777
+ fragColor = vec4(tonemapParticle(rgb) * alpha, alpha * (1.0 - clamp(additive, 0.0, 1.0)));
778
+ }
779
+ `