@luma.gl/shaders 10.0.0-alpha.10 → 10.0.0-alpha.12

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 (110) hide show
  1. package/dist/dist.dev.js +1923 -60
  2. package/dist/dist.min.js +1499 -148
  3. package/dist/glsl.cjs.map +1 -1
  4. package/dist/index.cjs +41 -0
  5. package/dist/index.cjs.map +3 -3
  6. package/dist/index.d.ts +20 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +11 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/modules/lighting/clustered-light-grid/clustered-light-grid.cjs +40 -0
  11. package/dist/modules/lighting/clustered-light-grid/clustered-light-grid.d.ts +6 -0
  12. package/dist/modules/lighting/clustered-light-grid/clustered-light-grid.d.ts.map +1 -0
  13. package/dist/modules/lighting/clustered-light-grid/clustered-light-grid.js +15 -0
  14. package/dist/modules/lighting/clustered-light-grid/clustered-light-grid.js.map +1 -0
  15. package/dist/modules/lighting/contact-shadow/contact-shadow.cjs +348 -0
  16. package/dist/modules/lighting/contact-shadow/contact-shadow.d.ts +188 -0
  17. package/dist/modules/lighting/contact-shadow/contact-shadow.d.ts.map +1 -0
  18. package/dist/modules/lighting/contact-shadow/contact-shadow.js +307 -0
  19. package/dist/modules/lighting/contact-shadow/contact-shadow.js.map +1 -0
  20. package/dist/modules/lighting/lightmap/lightmap-filter.cjs +93 -0
  21. package/dist/modules/lighting/lightmap/lightmap-filter.d.ts +35 -0
  22. package/dist/modules/lighting/lightmap/lightmap-filter.d.ts.map +1 -0
  23. package/dist/modules/lighting/lightmap/lightmap-filter.js +71 -0
  24. package/dist/modules/lighting/lightmap/lightmap-filter.js.map +1 -0
  25. package/dist/modules/lighting/lightmap/lightmap.cjs +89 -0
  26. package/dist/modules/lighting/lightmap/lightmap.d.ts +405 -0
  27. package/dist/modules/lighting/lightmap/lightmap.d.ts.map +1 -0
  28. package/dist/modules/lighting/lightmap/lightmap.js +67 -0
  29. package/dist/modules/lighting/lightmap/lightmap.js.map +1 -0
  30. package/dist/modules/lighting/pbr-ambient-occlusion/pbr-ambient-occlusion.cjs +87 -0
  31. package/dist/modules/lighting/pbr-ambient-occlusion/pbr-ambient-occlusion.d.ts +21 -0
  32. package/dist/modules/lighting/pbr-ambient-occlusion/pbr-ambient-occlusion.d.ts.map +1 -0
  33. package/dist/modules/lighting/pbr-ambient-occlusion/pbr-ambient-occlusion.js +61 -0
  34. package/dist/modules/lighting/pbr-ambient-occlusion/pbr-ambient-occlusion.js.map +1 -0
  35. package/dist/modules/lighting/pbr-contact-shadow/pbr-contact-shadow.cjs +87 -0
  36. package/dist/modules/lighting/pbr-contact-shadow/pbr-contact-shadow.d.ts +21 -0
  37. package/dist/modules/lighting/pbr-contact-shadow/pbr-contact-shadow.d.ts.map +1 -0
  38. package/dist/modules/lighting/pbr-contact-shadow/pbr-contact-shadow.js +61 -0
  39. package/dist/modules/lighting/pbr-contact-shadow/pbr-contact-shadow.js.map +1 -0
  40. package/dist/modules/lighting/pbr-material/pbr-material-glsl.cjs +33 -11
  41. package/dist/modules/lighting/pbr-material/pbr-material-glsl.d.ts +2 -2
  42. package/dist/modules/lighting/pbr-material/pbr-material-glsl.d.ts.map +1 -1
  43. package/dist/modules/lighting/pbr-material/pbr-material-glsl.js +33 -11
  44. package/dist/modules/lighting/pbr-material/pbr-material-glsl.js.map +1 -1
  45. package/dist/modules/lighting/pbr-material/pbr-material-wgsl.cjs +33 -11
  46. package/dist/modules/lighting/pbr-material/pbr-material-wgsl.d.ts +1 -1
  47. package/dist/modules/lighting/pbr-material/pbr-material-wgsl.d.ts.map +1 -1
  48. package/dist/modules/lighting/pbr-material/pbr-material-wgsl.js +33 -11
  49. package/dist/modules/lighting/pbr-material/pbr-material-wgsl.js.map +1 -1
  50. package/dist/modules/lighting/pbr-material/pbr-material.d.ts +3 -3
  51. package/dist/modules/lighting/pbr-shadow/pbr-shadow.cjs +97 -0
  52. package/dist/modules/lighting/pbr-shadow/pbr-shadow.d.ts +15 -0
  53. package/dist/modules/lighting/pbr-shadow/pbr-shadow.d.ts.map +1 -0
  54. package/dist/modules/lighting/pbr-shadow/pbr-shadow.js +69 -0
  55. package/dist/modules/lighting/pbr-shadow/pbr-shadow.js.map +1 -0
  56. package/dist/modules/lighting/pbr-surface/pbr-surface.cjs +114 -0
  57. package/dist/modules/lighting/pbr-surface/pbr-surface.d.ts +15 -0
  58. package/dist/modules/lighting/pbr-surface/pbr-surface.d.ts.map +1 -0
  59. package/dist/modules/lighting/pbr-surface/pbr-surface.js +88 -0
  60. package/dist/modules/lighting/pbr-surface/pbr-surface.js.map +1 -0
  61. package/dist/modules/lighting/prefiltered-environment/prefiltered-environment.cjs +379 -0
  62. package/dist/modules/lighting/prefiltered-environment/prefiltered-environment.d.ts +41 -0
  63. package/dist/modules/lighting/prefiltered-environment/prefiltered-environment.d.ts.map +1 -0
  64. package/dist/modules/lighting/prefiltered-environment/prefiltered-environment.js +345 -0
  65. package/dist/modules/lighting/prefiltered-environment/prefiltered-environment.js.map +1 -0
  66. package/dist/modules/lighting/shadow/shadow-glsl.cjs +228 -0
  67. package/dist/modules/lighting/shadow/shadow-glsl.d.ts +3 -0
  68. package/dist/modules/lighting/shadow/shadow-glsl.d.ts.map +1 -0
  69. package/dist/modules/lighting/shadow/shadow-glsl.js +204 -0
  70. package/dist/modules/lighting/shadow/shadow-glsl.js.map +1 -0
  71. package/dist/modules/lighting/shadow/shadow-types.cjs +15 -0
  72. package/dist/modules/lighting/shadow/shadow-types.d.ts +63 -0
  73. package/dist/modules/lighting/shadow/shadow-types.d.ts.map +1 -0
  74. package/dist/modules/lighting/shadow/shadow-types.js +5 -0
  75. package/dist/modules/lighting/shadow/shadow-types.js.map +1 -0
  76. package/dist/modules/lighting/shadow/shadow.cjs +458 -0
  77. package/dist/modules/lighting/shadow/shadow.d.ts +117 -0
  78. package/dist/modules/lighting/shadow/shadow.d.ts.map +1 -0
  79. package/dist/modules/lighting/shadow/shadow.js +450 -0
  80. package/dist/modules/lighting/shadow/shadow.js.map +1 -0
  81. package/dist/modules/lighting/unlit-material/unlit-material.cjs +60 -0
  82. package/dist/modules/lighting/unlit-material/unlit-material.d.ts +9 -0
  83. package/dist/modules/lighting/unlit-material/unlit-material.d.ts.map +1 -0
  84. package/dist/modules/lighting/unlit-material/unlit-material.js +40 -0
  85. package/dist/modules/lighting/unlit-material/unlit-material.js.map +1 -0
  86. package/dist/types/shader-type-decoder.cjs +12 -2
  87. package/dist/types/shader-type-decoder.d.ts +7 -7
  88. package/dist/types/shader-type-decoder.d.ts.map +1 -1
  89. package/dist/types/shader-type-decoder.js +13 -2
  90. package/dist/types/shader-type-decoder.js.map +1 -1
  91. package/dist/types.cjs.map +1 -1
  92. package/dist/wgsl.cjs.map +1 -1
  93. package/package.json +3 -3
  94. package/src/index.ts +52 -0
  95. package/src/modules/lighting/clustered-light-grid/clustered-light-grid.ts +17 -0
  96. package/src/modules/lighting/contact-shadow/contact-shadow.ts +365 -0
  97. package/src/modules/lighting/lightmap/lightmap-filter.ts +73 -0
  98. package/src/modules/lighting/lightmap/lightmap.ts +76 -0
  99. package/src/modules/lighting/pbr-ambient-occlusion/pbr-ambient-occlusion.ts +78 -0
  100. package/src/modules/lighting/pbr-contact-shadow/pbr-contact-shadow.ts +78 -0
  101. package/src/modules/lighting/pbr-material/pbr-material-glsl.ts +33 -11
  102. package/src/modules/lighting/pbr-material/pbr-material-wgsl.ts +33 -11
  103. package/src/modules/lighting/pbr-shadow/pbr-shadow.ts +85 -0
  104. package/src/modules/lighting/pbr-surface/pbr-surface.ts +103 -0
  105. package/src/modules/lighting/prefiltered-environment/prefiltered-environment.ts +370 -0
  106. package/src/modules/lighting/shadow/shadow-glsl.ts +204 -0
  107. package/src/modules/lighting/shadow/shadow-types.ts +68 -0
  108. package/src/modules/lighting/shadow/shadow.ts +536 -0
  109. package/src/modules/lighting/unlit-material/unlit-material.ts +44 -0
  110. package/src/types/shader-type-decoder.ts +22 -9
@@ -0,0 +1,536 @@
1
+ // luma.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
+
5
+ import {SHADOW_GLSL} from './shadow-glsl';
6
+ import type {NumberArray3, NumberArray4, NumberArray16} from '@math.gl/core';
7
+ import type {ShaderModule} from '../../../lib/shader-module/shader-module';
8
+ import type {
9
+ DirectionalShadowLight,
10
+ PointShadowLight,
11
+ ShadowProps,
12
+ SpotShadowLight
13
+ } from './shadow-types';
14
+
15
+ const MAX_CASCADES = 4;
16
+ const MAX_SPOT_LIGHTS = 4;
17
+ const MAX_POINT_LIGHTS = 4;
18
+ const MAX_POINT_MATRICES = 24;
19
+
20
+ type DirectionalShadowUniform = {
21
+ direction: Readonly<NumberArray3>;
22
+ strength: number;
23
+ normalBias: number;
24
+ sourceAngularRadius: number;
25
+ cascadeBlendFraction: number;
26
+ farFadeFraction: number;
27
+ shadowDistance: number;
28
+ };
29
+
30
+ type SpotShadowUniform = {
31
+ position: Readonly<NumberArray3>;
32
+ range: number;
33
+ direction: Readonly<NumberArray3>;
34
+ outerConeCos: number;
35
+ sourceRadius: number;
36
+ normalBias: number;
37
+ strength: number;
38
+ nearPlane: number;
39
+ };
40
+
41
+ type PointShadowUniform = {
42
+ position: Readonly<NumberArray3>;
43
+ range: number;
44
+ sourceRadius: number;
45
+ normalBias: number;
46
+ strength: number;
47
+ nearPlane: number;
48
+ };
49
+
50
+ export type ShadowUniforms = {
51
+ directionalLightCount: number;
52
+ spotLightCount: number;
53
+ pointLightCount: number;
54
+ cascadeCount: number;
55
+ blockerSampleCount: number;
56
+ filterSampleCount: number;
57
+ cascadeSplits: Readonly<NumberArray4>;
58
+ directionalViewProjectionMatrices: readonly Readonly<NumberArray16>[];
59
+ spotViewProjectionMatrices: readonly Readonly<NumberArray16>[];
60
+ pointViewProjectionMatrices: readonly Readonly<NumberArray16>[];
61
+ directionalLights: readonly DirectionalShadowUniform[];
62
+ spotLights: readonly SpotShadowUniform[];
63
+ pointLights: readonly PointShadowUniform[];
64
+ };
65
+
66
+ export type ShadowBindings<TextureT = unknown, SamplerT = unknown> = {
67
+ directionalShadowTexture: TextureT;
68
+ spotShadowTexture: TextureT;
69
+ pointShadowTexture: TextureT;
70
+ shadowComparisonSampler?: SamplerT;
71
+ };
72
+
73
+ const DIRECTIONAL_LIGHT_UNIFORM_TYPE = {
74
+ direction: 'vec3<f32>',
75
+ strength: 'f32',
76
+ normalBias: 'f32',
77
+ sourceAngularRadius: 'f32',
78
+ cascadeBlendFraction: 'f32',
79
+ farFadeFraction: 'f32',
80
+ shadowDistance: 'f32'
81
+ } as const;
82
+
83
+ const SPOT_LIGHT_UNIFORM_TYPE = {
84
+ position: 'vec3<f32>',
85
+ range: 'f32',
86
+ direction: 'vec3<f32>',
87
+ outerConeCos: 'f32',
88
+ sourceRadius: 'f32',
89
+ normalBias: 'f32',
90
+ strength: 'f32',
91
+ nearPlane: 'f32'
92
+ } as const;
93
+
94
+ const POINT_LIGHT_UNIFORM_TYPE = {
95
+ position: 'vec3<f32>',
96
+ range: 'f32',
97
+ sourceRadius: 'f32',
98
+ normalBias: 'f32',
99
+ strength: 'f32',
100
+ nearPlane: 'f32'
101
+ } as const;
102
+
103
+ const SHADOW_WGSL = /* wgsl */ `\
104
+ const SHADOW_PI: f32 = 3.141592653589793;
105
+
106
+ struct DirectionalShadowLightUniform {
107
+ direction: vec3f,
108
+ strength: f32,
109
+ normalBias: f32,
110
+ sourceAngularRadius: f32,
111
+ cascadeBlendFraction: f32,
112
+ farFadeFraction: f32,
113
+ shadowDistance: f32,
114
+ };
115
+
116
+ struct SpotShadowLightUniform {
117
+ position: vec3f,
118
+ range: f32,
119
+ direction: vec3f,
120
+ outerConeCos: f32,
121
+ sourceRadius: f32,
122
+ normalBias: f32,
123
+ strength: f32,
124
+ nearPlane: f32,
125
+ };
126
+
127
+ struct PointShadowLightUniform {
128
+ position: vec3f,
129
+ range: f32,
130
+ sourceRadius: f32,
131
+ normalBias: f32,
132
+ strength: f32,
133
+ nearPlane: f32,
134
+ };
135
+
136
+ struct ShadowUniforms {
137
+ directionalLightCount: i32,
138
+ spotLightCount: i32,
139
+ pointLightCount: i32,
140
+ cascadeCount: i32,
141
+ blockerSampleCount: i32,
142
+ filterSampleCount: i32,
143
+ cascadeSplits: vec4f,
144
+ directionalViewProjectionMatrices: array<mat4x4f, 4>,
145
+ spotViewProjectionMatrices: array<mat4x4f, 4>,
146
+ pointViewProjectionMatrices: array<mat4x4f, 24>,
147
+ directionalLights: array<DirectionalShadowLightUniform, 1>,
148
+ spotLights: array<SpotShadowLightUniform, 4>,
149
+ pointLights: array<PointShadowLightUniform, 4>,
150
+ };
151
+
152
+ @group(2) @binding(auto) var<uniform> shadow: ShadowUniforms;
153
+ @group(2) @binding(auto) var directionalShadowTexture: texture_depth_2d_array;
154
+ @group(2) @binding(auto) var spotShadowTexture: texture_depth_2d_array;
155
+ @group(2) @binding(auto) var pointShadowTexture: texture_depth_cube_array;
156
+ @group(2) @binding(auto) var directionalShadowTextureSampler: sampler;
157
+ @group(2) @binding(auto) var shadowComparisonSampler: sampler_comparison;
158
+
159
+ fn shadow_hash(position: vec3f) -> f32 {
160
+ return fract(sin(dot(position, vec3f(12.9898, 78.233, 37.719))) * 43758.5453);
161
+ }
162
+
163
+ fn shadow_diskSample(index: i32, count: i32, rotation: f32) -> vec2f {
164
+ let fraction = (f32(index) + 0.5) / max(f32(count), 1.0);
165
+ let angle = f32(index) * 2.39996323 + rotation;
166
+ return sqrt(fraction) * vec2f(cos(angle), sin(angle));
167
+ }
168
+
169
+ fn shadow_project(matrix: mat4x4f, worldPosition: vec3f) -> vec3f {
170
+ let clip = matrix * vec4f(worldPosition, 1.0);
171
+ let ndc = clip.xyz / max(abs(clip.w), 0.00001);
172
+ return vec3f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5, ndc.z);
173
+ }
174
+
175
+ fn shadow_validProjection(projected: vec3f) -> bool {
176
+ return all(projected.xy >= vec2f(0.0)) && all(projected.xy <= vec2f(1.0)) &&
177
+ projected.z >= 0.0 && projected.z <= 1.0;
178
+ }
179
+
180
+ fn shadow_directionalCascadeIndex(viewDepth: f32) -> i32 {
181
+ var cascadeIndex = 0;
182
+ for (var index: i32 = 0; index < 4; index++) {
183
+ if (index >= shadow.cascadeCount) { break; }
184
+ cascadeIndex = index;
185
+ if (viewDepth <= shadow.cascadeSplits[index]) { break; }
186
+ }
187
+ return cascadeIndex;
188
+ }
189
+
190
+ fn shadow_getDirectionalCascadeIndex(viewDepth: f32) -> i32 {
191
+ if (shadow.directionalLightCount == 0) { return -1; }
192
+ return shadow_directionalCascadeIndex(viewDepth);
193
+ }
194
+
195
+ fn shadow_directionalPCSS(cascadeIndex: i32, worldPosition: vec3f, viewDepth: f32) -> f32 {
196
+ let projected = shadow_project(shadow.directionalViewProjectionMatrices[cascadeIndex], worldPosition);
197
+ if (!shadow_validProjection(projected)) { return 1.0; }
198
+ let dimensions = vec2f(textureDimensions(directionalShadowTexture));
199
+ let texel = 1.0 / dimensions;
200
+ let light = shadow.directionalLights[0];
201
+ let rotation = shadow_hash(worldPosition) * 2.0 * SHADOW_PI;
202
+ let searchRadius = max(texel.x, light.sourceAngularRadius * viewDepth / max(light.shadowDistance, 0.001));
203
+ var blockerDepth = 0.0;
204
+ var blockerCount = 0.0;
205
+ for (var index: i32 = 0; index < 24; index++) {
206
+ if (index >= shadow.blockerSampleCount) { break; }
207
+ let offset = shadow_diskSample(index, shadow.blockerSampleCount, rotation) * searchRadius;
208
+ let sampleDepth = textureSampleLevel(
209
+ directionalShadowTexture,
210
+ directionalShadowTextureSampler,
211
+ clamp(projected.xy + offset, vec2f(0.0), vec2f(1.0)),
212
+ cascadeIndex,
213
+ 0
214
+ );
215
+ if (sampleDepth < projected.z) {
216
+ blockerDepth += sampleDepth;
217
+ blockerCount += 1.0;
218
+ }
219
+ }
220
+ if (blockerCount == 0.0) { return 1.0; }
221
+ let averageBlocker = blockerDepth / blockerCount;
222
+ let penumbra = clamp((projected.z - averageBlocker) / max(abs(averageBlocker), 0.001), 0.0, 1.0);
223
+ let filterRadius = max(texel.x, searchRadius * (1.0 + penumbra * 18.0));
224
+ var visibility = 0.0;
225
+ for (var index: i32 = 0; index < 48; index++) {
226
+ if (index >= shadow.filterSampleCount) { break; }
227
+ let offset = shadow_diskSample(index, shadow.filterSampleCount, rotation) * filterRadius;
228
+ visibility += textureSampleCompareLevel(
229
+ directionalShadowTexture,
230
+ shadowComparisonSampler,
231
+ clamp(projected.xy + offset, vec2f(0.0), vec2f(1.0)),
232
+ cascadeIndex,
233
+ projected.z
234
+ );
235
+ }
236
+ return visibility / max(f32(shadow.filterSampleCount), 1.0);
237
+ }
238
+
239
+ fn shadow_getDirectionalFactor(worldPosition: vec3f, worldNormal: vec3f, viewDepth: f32) -> f32 {
240
+ if (shadow.directionalLightCount == 0 || viewDepth <= 0.0) { return 1.0; }
241
+ let light = shadow.directionalLights[0];
242
+ if (viewDepth >= light.shadowDistance) { return 1.0; }
243
+ let biasedPosition = worldPosition + normalize(worldNormal) * light.normalBias;
244
+ let cascadeIndex = shadow_directionalCascadeIndex(viewDepth);
245
+ var visibility = shadow_directionalPCSS(cascadeIndex, biasedPosition, viewDepth);
246
+ if (cascadeIndex + 1 < shadow.cascadeCount) {
247
+ var cascadeStart = 0.0;
248
+ if (cascadeIndex > 0) { cascadeStart = shadow.cascadeSplits[cascadeIndex - 1]; }
249
+ let cascadeEnd = shadow.cascadeSplits[cascadeIndex];
250
+ let blendWidth = max((cascadeEnd - cascadeStart) * light.cascadeBlendFraction, 0.0001);
251
+ let blend = smoothstep(cascadeEnd - blendWidth, cascadeEnd, viewDepth);
252
+ if (blend > 0.0) {
253
+ visibility = mix(
254
+ visibility,
255
+ shadow_directionalPCSS(cascadeIndex + 1, biasedPosition, viewDepth),
256
+ blend
257
+ );
258
+ }
259
+ }
260
+ let fadeWidth = max(light.shadowDistance * light.farFadeFraction, 0.0001);
261
+ let farFade = smoothstep(light.shadowDistance - fadeWidth, light.shadowDistance, viewDepth);
262
+ visibility = mix(visibility, 1.0, farFade);
263
+ return mix(1.0, visibility, light.strength);
264
+ }
265
+
266
+ fn shadow_spotPCSS(lightIndex: i32, worldPosition: vec3f) -> f32 {
267
+ let projected = shadow_project(shadow.spotViewProjectionMatrices[lightIndex], worldPosition);
268
+ if (!shadow_validProjection(projected)) { return 1.0; }
269
+ let light = shadow.spotLights[lightIndex];
270
+ let dimensions = vec2f(textureDimensions(spotShadowTexture));
271
+ let texel = 1.0 / dimensions;
272
+ let distanceToLight = length(worldPosition - light.position);
273
+ let searchRadius = max(texel.x, light.sourceRadius / max(distanceToLight, 0.001) * 0.5);
274
+ let rotation = shadow_hash(worldPosition) * 2.0 * SHADOW_PI;
275
+ var blockerDepth = 0.0;
276
+ var blockerCount = 0.0;
277
+ for (var index: i32 = 0; index < 24; index++) {
278
+ if (index >= shadow.blockerSampleCount) { break; }
279
+ let offset = shadow_diskSample(index, shadow.blockerSampleCount, rotation) * searchRadius;
280
+ let sampleDepth = textureSampleLevel(
281
+ spotShadowTexture,
282
+ directionalShadowTextureSampler,
283
+ clamp(projected.xy + offset, vec2f(0.0), vec2f(1.0)),
284
+ lightIndex,
285
+ 0
286
+ );
287
+ if (sampleDepth < projected.z) { blockerDepth += sampleDepth; blockerCount += 1.0; }
288
+ }
289
+ if (blockerCount == 0.0) { return 1.0; }
290
+ let averageBlocker = blockerDepth / blockerCount;
291
+ let penumbra = clamp((projected.z - averageBlocker) / max(abs(averageBlocker), 0.001), 0.0, 1.0);
292
+ let filterRadius = max(texel.x, searchRadius * (1.0 + penumbra * 16.0));
293
+ var visibility = 0.0;
294
+ for (var index: i32 = 0; index < 48; index++) {
295
+ if (index >= shadow.filterSampleCount) { break; }
296
+ let offset = shadow_diskSample(index, shadow.filterSampleCount, rotation) * filterRadius;
297
+ visibility += textureSampleCompareLevel(
298
+ spotShadowTexture,
299
+ shadowComparisonSampler,
300
+ clamp(projected.xy + offset, vec2f(0.0), vec2f(1.0)),
301
+ lightIndex,
302
+ projected.z
303
+ );
304
+ }
305
+ return visibility / max(f32(shadow.filterSampleCount), 1.0);
306
+ }
307
+
308
+ fn shadow_getSpotFactor(lightIndex: i32, worldPosition: vec3f, worldNormal: vec3f) -> f32 {
309
+ if (lightIndex < 0 || lightIndex >= shadow.spotLightCount) { return 1.0; }
310
+ let light = shadow.spotLights[lightIndex];
311
+ let fromLight = worldPosition - light.position;
312
+ let distanceToLight = length(fromLight);
313
+ let coneCos = dot(normalize(fromLight), normalize(light.direction));
314
+ if (distanceToLight >= light.range || coneCos < light.outerConeCos) { return 1.0; }
315
+ let biasedPosition = worldPosition + normalize(worldNormal) * light.normalBias;
316
+ return mix(1.0, shadow_spotPCSS(lightIndex, biasedPosition), light.strength);
317
+ }
318
+
319
+ fn shadow_pointReferenceDepth(majorDistance: f32, nearPlane: f32, farPlane: f32) -> f32 {
320
+ // Match the cube-face projection and stored WebGPU depth in [0, 1].
321
+ return farPlane / (farPlane - nearPlane) -
322
+ (farPlane * nearPlane) / ((farPlane - nearPlane) * majorDistance);
323
+ }
324
+
325
+ fn shadow_pointBasis(direction: vec3f) -> mat2x3f {
326
+ let reference = select(vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0), abs(direction.y) > 0.99);
327
+ let tangent = normalize(cross(direction, reference));
328
+ return mat2x3f(tangent, normalize(cross(direction, tangent)));
329
+ }
330
+
331
+ fn shadow_getPointFactor(lightIndex: i32, worldPosition: vec3f, worldNormal: vec3f) -> f32 {
332
+ if (lightIndex < 0 || lightIndex >= shadow.pointLightCount) { return 1.0; }
333
+ let light = shadow.pointLights[lightIndex];
334
+ let biasedPosition = worldPosition + normalize(worldNormal) * light.normalBias;
335
+ let fromLight = biasedPosition - light.position;
336
+ let distanceToLight = length(fromLight);
337
+ if (distanceToLight >= light.range || distanceToLight <= light.nearPlane) { return 1.0; }
338
+ let direction = normalize(fromLight);
339
+ let majorDistance = max(max(abs(fromLight.x), abs(fromLight.y)), abs(fromLight.z));
340
+ let referenceDepth = shadow_pointReferenceDepth(majorDistance, light.nearPlane, light.range);
341
+ let searchRadius = light.sourceRadius / max(distanceToLight, 0.001);
342
+ let rotation = shadow_hash(worldPosition) * 2.0 * SHADOW_PI;
343
+ let basis = shadow_pointBasis(direction);
344
+ var blockerDepth = 0.0;
345
+ var blockerCount = 0.0;
346
+ for (var index: i32 = 0; index < 24; index++) {
347
+ if (index >= shadow.blockerSampleCount) { break; }
348
+ let disk = shadow_diskSample(index, shadow.blockerSampleCount, rotation) * searchRadius;
349
+ let sampleDirection = normalize(direction + basis[0] * disk.x + basis[1] * disk.y);
350
+ let sampleDepth = textureSampleLevel(
351
+ pointShadowTexture,
352
+ directionalShadowTextureSampler,
353
+ sampleDirection,
354
+ lightIndex,
355
+ 0
356
+ );
357
+ if (sampleDepth < referenceDepth) { blockerDepth += sampleDepth; blockerCount += 1.0; }
358
+ }
359
+ if (blockerCount == 0.0) { return 1.0; }
360
+ let averageBlocker = blockerDepth / blockerCount;
361
+ let penumbra = clamp((referenceDepth - averageBlocker) / max(abs(averageBlocker), 0.001), 0.0, 1.0);
362
+ let filterRadius = searchRadius * (1.0 + penumbra * 12.0);
363
+ var visibility = 0.0;
364
+ for (var index: i32 = 0; index < 48; index++) {
365
+ if (index >= shadow.filterSampleCount) { break; }
366
+ let disk = shadow_diskSample(index, shadow.filterSampleCount, rotation) * filterRadius;
367
+ let sampleDirection = normalize(direction + basis[0] * disk.x + basis[1] * disk.y);
368
+ visibility += textureSampleCompareLevel(
369
+ pointShadowTexture,
370
+ shadowComparisonSampler,
371
+ sampleDirection,
372
+ lightIndex,
373
+ referenceDepth
374
+ );
375
+ }
376
+ visibility /= max(f32(shadow.filterSampleCount), 1.0);
377
+ return mix(1.0, visibility, light.strength);
378
+ }
379
+ `;
380
+
381
+ /** Backend-aware shadow sampling module. Applications explicitly apply factors to direct-light terms. */
382
+ export const shadow = {
383
+ name: 'shadow',
384
+ source: SHADOW_WGSL,
385
+ fs: SHADOW_GLSL,
386
+ props: {} as ShadowProps,
387
+ uniforms: {} as ShadowUniforms,
388
+ bindings: {} as ShadowBindings,
389
+ uniformTypes: {
390
+ directionalLightCount: 'i32',
391
+ spotLightCount: 'i32',
392
+ pointLightCount: 'i32',
393
+ cascadeCount: 'i32',
394
+ blockerSampleCount: 'i32',
395
+ filterSampleCount: 'i32',
396
+ cascadeSplits: 'vec4<f32>',
397
+ directionalViewProjectionMatrices: ['mat4x4<f32>', MAX_CASCADES],
398
+ spotViewProjectionMatrices: ['mat4x4<f32>', MAX_SPOT_LIGHTS],
399
+ pointViewProjectionMatrices: ['mat4x4<f32>', MAX_POINT_MATRICES],
400
+ directionalLights: [DIRECTIONAL_LIGHT_UNIFORM_TYPE, 1],
401
+ spotLights: [SPOT_LIGHT_UNIFORM_TYPE, MAX_SPOT_LIGHTS],
402
+ pointLights: [POINT_LIGHT_UNIFORM_TYPE, MAX_POINT_LIGHTS]
403
+ },
404
+ bindingLayout: [
405
+ {name: 'shadow', group: 2},
406
+ {name: 'directionalShadowTexture', group: 2},
407
+ {name: 'spotShadowTexture', group: 2},
408
+ {name: 'pointShadowTexture', group: 2},
409
+ {name: 'shadowComparisonSampler', group: 2}
410
+ ],
411
+ getUniforms(props: Partial<ShadowProps>): Partial<ShadowUniforms & ShadowBindings> {
412
+ if (
413
+ !props.directionalLights ||
414
+ !props.spotLights ||
415
+ !props.pointLights ||
416
+ !props.cascadeSplits ||
417
+ !props.directionalViewProjectionMatrices ||
418
+ !props.spotViewProjectionMatrices ||
419
+ !props.pointViewProjectionMatrices ||
420
+ !props.directionalShadowTexture ||
421
+ !props.spotShadowTexture ||
422
+ !props.pointShadowTexture ||
423
+ !props.nonFilteringSampler ||
424
+ !props.comparisonSampler
425
+ ) {
426
+ return {};
427
+ }
428
+ return {
429
+ directionalLightCount: props.directionalLights.length,
430
+ spotLightCount: props.spotLights.length,
431
+ pointLightCount: props.pointLights.length,
432
+ cascadeCount: props.cascadeCount,
433
+ blockerSampleCount: props.blockerSampleCount,
434
+ filterSampleCount: props.filterSampleCount,
435
+ cascadeSplits: props.cascadeSplits as Readonly<NumberArray4>,
436
+ directionalViewProjectionMatrices:
437
+ props.directionalViewProjectionMatrices as readonly Readonly<NumberArray16>[],
438
+ spotViewProjectionMatrices:
439
+ props.spotViewProjectionMatrices as readonly Readonly<NumberArray16>[],
440
+ pointViewProjectionMatrices:
441
+ props.pointViewProjectionMatrices as readonly Readonly<NumberArray16>[],
442
+ directionalLights: padDirectionalLights(props.directionalLights),
443
+ spotLights: padSpotLights(props.spotLights),
444
+ pointLights: padPointLights(props.pointLights),
445
+ directionalShadowTexture: props.directionalShadowTexture,
446
+ spotShadowTexture: props.spotShadowTexture,
447
+ pointShadowTexture: props.pointShadowTexture,
448
+ ...(props.directionalShadowTexture.device.type === 'webgpu'
449
+ ? {shadowComparisonSampler: props.comparisonSampler}
450
+ : {})
451
+ };
452
+ }
453
+ } as const satisfies ShaderModule<ShadowProps, ShadowUniforms, ShadowBindings>;
454
+
455
+ function padDirectionalLights(
456
+ lights: readonly DirectionalShadowLight[]
457
+ ): DirectionalShadowUniform[] {
458
+ const light = lights[0];
459
+ return [
460
+ light
461
+ ? {
462
+ direction: light.direction,
463
+ strength: light.strength ?? 1,
464
+ normalBias: light.normalBias ?? 0.04,
465
+ sourceAngularRadius: light.sourceAngularRadius ?? 0.00465,
466
+ cascadeBlendFraction: light.cascadeBlendFraction ?? 0.1,
467
+ farFadeFraction: light.farFadeFraction ?? 0.1,
468
+ shadowDistance: light.shadowDistance ?? 1
469
+ }
470
+ : {
471
+ direction: [0, 1, 0],
472
+ strength: 0,
473
+ normalBias: 0,
474
+ sourceAngularRadius: 0,
475
+ cascadeBlendFraction: 0,
476
+ farFadeFraction: 0,
477
+ shadowDistance: 1
478
+ }
479
+ ];
480
+ }
481
+
482
+ function padSpotLights(lights: readonly SpotShadowLight[]): SpotShadowUniform[] {
483
+ return padArray(
484
+ lights.map(light => ({
485
+ position: light.position,
486
+ range: light.range,
487
+ direction: light.direction,
488
+ outerConeCos: Math.cos(light.outerConeAngle),
489
+ sourceRadius: light.sourceRadius ?? 0.2,
490
+ normalBias: light.normalBias ?? 0.025,
491
+ strength: light.strength ?? 1,
492
+ nearPlane: light.nearPlane ?? 0.1
493
+ })),
494
+ MAX_SPOT_LIGHTS,
495
+ {
496
+ position: [0, 0, 0],
497
+ range: 1,
498
+ direction: [0, -1, 0],
499
+ outerConeCos: 1,
500
+ sourceRadius: 0,
501
+ normalBias: 0,
502
+ strength: 0,
503
+ nearPlane: 0.1
504
+ }
505
+ );
506
+ }
507
+
508
+ function padPointLights(lights: readonly PointShadowLight[]): PointShadowUniform[] {
509
+ return padArray(
510
+ lights.map(light => ({
511
+ position: light.position,
512
+ range: light.range,
513
+ sourceRadius: light.sourceRadius ?? 0.2,
514
+ normalBias: light.normalBias ?? 0.025,
515
+ strength: light.strength ?? 1,
516
+ nearPlane: light.nearPlane ?? 0.1
517
+ })),
518
+ MAX_POINT_LIGHTS,
519
+ {
520
+ position: [0, 0, 0],
521
+ range: 1,
522
+ sourceRadius: 0,
523
+ normalBias: 0,
524
+ strength: 0,
525
+ nearPlane: 0.1
526
+ }
527
+ );
528
+ }
529
+
530
+ function padArray<T>(values: readonly T[], length: number, fallback: T): T[] {
531
+ const result = values.slice(0, length);
532
+ while (result.length < length) {
533
+ result.push(fallback);
534
+ }
535
+ return result;
536
+ }
@@ -0,0 +1,44 @@
1
+ // luma.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
+
5
+ import type {ShaderModule} from '../../../lib/shader-module/shader-module';
6
+
7
+ const SOURCE_WGSL = /* wgsl */ `\
8
+ fn unlitMaterial_getColor(surfaceColor: vec4<f32>) -> vec4<f32> {
9
+ return surfaceColor;
10
+ }
11
+
12
+ // Compatibility with the aggregate Phong/Gouraud material evaluation function.
13
+ fn lighting_getLightColor2(surfaceColor: vec3<f32>, cameraPosition: vec3<f32>, positionWorldspace: vec3<f32>, normalWorldspace: vec3<f32>) -> vec3<f32> {
14
+ return surfaceColor;
15
+ }
16
+ `;
17
+
18
+ const SOURCE_GLSL = /* glsl */ `\
19
+ vec4 unlitMaterial_getColor(vec4 surfaceColor) {
20
+ return surfaceColor;
21
+ }
22
+
23
+ // Compatibility with the aggregate Phong/Gouraud material evaluation function.
24
+ vec3 lighting_getLightColor(vec3 surfaceColor, vec3 cameraPosition, vec3 positionWorldspace, vec3 normalWorldspace) {
25
+ return surfaceColor;
26
+ }
27
+ `;
28
+
29
+ /**
30
+ * Preserves surface RGBA without lighting dependencies, uniforms, or bindings.
31
+ * Use instead of a lit material module when lighting is never needed. The caller
32
+ * supplies color/texture sampling and remains responsible for output color space
33
+ * and alpha conventions. Available in both vertex and fragment stages.
34
+ */
35
+ export const unlitMaterial: ShaderModule<Record<string, never>> = {
36
+ name: 'unlitMaterial',
37
+ source: SOURCE_WGSL,
38
+ vs: SOURCE_GLSL,
39
+ fs: SOURCE_GLSL,
40
+ dependencies: [],
41
+ bindingLayout: [],
42
+ uniformTypes: {},
43
+ defaultUniforms: {}
44
+ };
@@ -12,22 +12,22 @@ import {
12
12
 
13
13
  /** Information extracted from a VariableShaderType constant */
14
14
  export type VariableShaderTypeInfo = {
15
- type: PrimitiveDataType;
16
- components: number;
15
+ readonly type: PrimitiveDataType;
16
+ readonly components: number;
17
17
  };
18
18
 
19
19
  /** Information extracted from a AttributeShaderType constant */
20
20
  export type AttributeShaderTypeInfo = {
21
21
  /** WGSL-style primitive data type, f32, i32, u32 */
22
- primitiveType: PrimitiveDataType;
22
+ readonly primitiveType: PrimitiveDataType;
23
23
  /** Whether this is a normalized integer (that must be used as float) */
24
- components: 1 | 2 | 3 | 4;
24
+ readonly components: 1 | 2 | 3 | 4;
25
25
  /** Length in bytes of the data for one vertex */
26
- byteLength?: number;
26
+ readonly byteLength?: number;
27
27
  /** Whether this is for integer or float vert */
28
- integer: boolean;
28
+ readonly integer: boolean;
29
29
  /** Whether this data type is signed */
30
- signed: boolean;
30
+ readonly signed: boolean;
31
31
  };
32
32
 
33
33
  /** Split a uniform type string into type and components */
@@ -42,11 +42,17 @@ export function getVariableShaderTypeInfo(
42
42
  return decoded;
43
43
  }
44
44
 
45
+ const attributeShaderTypeInfoCache = new Map<AttributeShaderType, AttributeShaderTypeInfo>();
46
+
45
47
  /** Decodes a vertex type, returning byte length and flags (integer, signed, normalized) */
46
48
  export function getAttributeShaderTypeInfo(
47
49
  attributeType: AttributeShaderType | AttributeShaderTypeAlias
48
50
  ): AttributeShaderTypeInfo {
49
51
  const resolvedAttributeType = resolveAttributeShaderTypeAlias(attributeType);
52
+ const cached = attributeShaderTypeInfoCache.get(resolvedAttributeType);
53
+ if (cached) {
54
+ return cached;
55
+ }
50
56
  const decoded = TYPE_INFO[resolvedAttributeType];
51
57
  if (!decoded) {
52
58
  throw new Error(`Unsupported attribute shader type: ${attributeType}`);
@@ -56,13 +62,15 @@ export function getAttributeShaderTypeInfo(
56
62
  const signed: boolean = primitiveType !== 'u32';
57
63
 
58
64
  const byteLength = PRIMITIVE_TYPE_SIZES[primitiveType] * components;
59
- return {
65
+ const result = Object.freeze({
60
66
  primitiveType,
61
67
  components,
62
68
  byteLength,
63
69
  integer,
64
70
  signed
65
- };
71
+ });
72
+ attributeShaderTypeInfoCache.set(resolvedAttributeType, result);
73
+ return result;
66
74
  }
67
75
 
68
76
  export class ShaderTypeDecoder {
@@ -211,6 +219,11 @@ const UNIFORM_FORMATS: Record<VariableShaderType, {type: PrimitiveDataType; comp
211
219
  'mat4x4<u32>': {type: 'u32', components: 16}
212
220
  };
213
221
 
222
+ // Variable metadata is already allocated in the table; freeze it once, without another cache.
223
+ for (const info of Object.values(UNIFORM_FORMATS)) {
224
+ Object.freeze(info);
225
+ }
226
+
214
227
  /** Predeclared aliases @see https://www.w3.org/TR/WGSL/#vector-types */
215
228
  export const WGSL_ATTRIBUTE_TYPE_ALIAS_MAP: Record<AttributeShaderTypeAlias, AttributeShaderType> =
216
229
  {