@luma.gl/gltf 9.3.0-alpha.4 → 9.3.0-alpha.8

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 (90) hide show
  1. package/dist/dist.dev.js +1305 -327
  2. package/dist/dist.min.js +98 -46
  3. package/dist/gltf/animations/animations.d.ts +16 -4
  4. package/dist/gltf/animations/animations.d.ts.map +1 -1
  5. package/dist/gltf/animations/interpolate.d.ts +4 -3
  6. package/dist/gltf/animations/interpolate.d.ts.map +1 -1
  7. package/dist/gltf/animations/interpolate.js +27 -36
  8. package/dist/gltf/animations/interpolate.js.map +1 -1
  9. package/dist/gltf/create-gltf-model.d.ts +15 -1
  10. package/dist/gltf/create-gltf-model.d.ts.map +1 -1
  11. package/dist/gltf/create-gltf-model.js +154 -48
  12. package/dist/gltf/create-gltf-model.js.map +1 -1
  13. package/dist/gltf/create-scenegraph-from-gltf.d.ts +37 -2
  14. package/dist/gltf/create-scenegraph-from-gltf.d.ts.map +1 -1
  15. package/dist/gltf/create-scenegraph-from-gltf.js +74 -6
  16. package/dist/gltf/create-scenegraph-from-gltf.js.map +1 -1
  17. package/dist/gltf/gltf-animator.d.ts +26 -0
  18. package/dist/gltf/gltf-animator.d.ts.map +1 -1
  19. package/dist/gltf/gltf-animator.js +22 -19
  20. package/dist/gltf/gltf-animator.js.map +1 -1
  21. package/dist/gltf/gltf-extension-support.d.ts +10 -0
  22. package/dist/gltf/gltf-extension-support.d.ts.map +1 -0
  23. package/dist/gltf/gltf-extension-support.js +173 -0
  24. package/dist/gltf/gltf-extension-support.js.map +1 -0
  25. package/dist/index.cjs +1247 -294
  26. package/dist/index.cjs.map +4 -4
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/parsers/parse-gltf-animations.d.ts +1 -0
  32. package/dist/parsers/parse-gltf-animations.d.ts.map +1 -1
  33. package/dist/parsers/parse-gltf-animations.js +73 -28
  34. package/dist/parsers/parse-gltf-animations.js.map +1 -1
  35. package/dist/parsers/parse-gltf-lights.d.ts.map +1 -1
  36. package/dist/parsers/parse-gltf-lights.js +112 -18
  37. package/dist/parsers/parse-gltf-lights.js.map +1 -1
  38. package/dist/parsers/parse-gltf.d.ts +19 -2
  39. package/dist/parsers/parse-gltf.d.ts.map +1 -1
  40. package/dist/parsers/parse-gltf.js +101 -61
  41. package/dist/parsers/parse-gltf.js.map +1 -1
  42. package/dist/parsers/parse-pbr-material.d.ts +115 -2
  43. package/dist/parsers/parse-pbr-material.d.ts.map +1 -1
  44. package/dist/parsers/parse-pbr-material.js +565 -54
  45. package/dist/parsers/parse-pbr-material.js.map +1 -1
  46. package/dist/pbr/pbr-environment.d.ts +6 -0
  47. package/dist/pbr/pbr-environment.d.ts.map +1 -1
  48. package/dist/pbr/pbr-environment.js +15 -12
  49. package/dist/pbr/pbr-environment.js.map +1 -1
  50. package/dist/pbr/pbr-material.d.ts +13 -3
  51. package/dist/pbr/pbr-material.d.ts.map +1 -1
  52. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts +12 -1
  53. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts.map +1 -1
  54. package/dist/webgl-to-webgpu/convert-webgl-attribute.js +3 -0
  55. package/dist/webgl-to-webgpu/convert-webgl-attribute.js.map +1 -1
  56. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts +11 -5
  57. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts.map +1 -1
  58. package/dist/webgl-to-webgpu/convert-webgl-sampler.js +16 -12
  59. package/dist/webgl-to-webgpu/convert-webgl-sampler.js.map +1 -1
  60. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts +2 -9
  61. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts.map +1 -1
  62. package/dist/webgl-to-webgpu/convert-webgl-topology.js +2 -14
  63. package/dist/webgl-to-webgpu/convert-webgl-topology.js.map +1 -1
  64. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts +27 -0
  65. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts.map +1 -0
  66. package/dist/webgl-to-webgpu/gltf-webgl-constants.js +34 -0
  67. package/dist/webgl-to-webgpu/gltf-webgl-constants.js.map +1 -0
  68. package/package.json +8 -9
  69. package/src/gltf/animations/animations.ts +17 -5
  70. package/src/gltf/animations/interpolate.ts +49 -68
  71. package/src/gltf/create-gltf-model.ts +214 -48
  72. package/src/gltf/create-scenegraph-from-gltf.ts +131 -12
  73. package/src/gltf/gltf-animator.ts +34 -25
  74. package/src/gltf/gltf-extension-support.ts +214 -0
  75. package/src/index.ts +10 -2
  76. package/src/parsers/parse-gltf-animations.ts +94 -33
  77. package/src/parsers/parse-gltf-lights.ts +147 -20
  78. package/src/parsers/parse-gltf.ts +170 -90
  79. package/src/parsers/parse-pbr-material.ts +865 -80
  80. package/src/pbr/pbr-environment.ts +38 -15
  81. package/src/pbr/pbr-material.ts +18 -3
  82. package/src/webgl-to-webgpu/convert-webgl-attribute.ts +12 -1
  83. package/src/webgl-to-webgpu/convert-webgl-sampler.ts +38 -29
  84. package/src/webgl-to-webgpu/convert-webgl-topology.ts +2 -14
  85. package/src/webgl-to-webgpu/gltf-webgl-constants.ts +35 -0
  86. package/dist/utils/deep-copy.d.ts +0 -3
  87. package/dist/utils/deep-copy.d.ts.map +0 -1
  88. package/dist/utils/deep-copy.js +0 -21
  89. package/dist/utils/deep-copy.js.map +0 -1
  90. package/src/utils/deep-copy.ts +0 -22
@@ -2,14 +2,14 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {Device, Texture} from '@luma.gl/core';
6
- import type {GLTFSampler} from '@loaders.gl/gltf';
7
- import {GL} from '@luma.gl/constants';
5
+ import type {Device, SamplerProps, TextureFormat, TypedArray} from '@luma.gl/core';
6
+ import {Texture, log, textureFormatDecoder} from '@luma.gl/core';
7
+ import type {GLTFPostprocessed, GLTFSampler} from '@loaders.gl/gltf';
8
8
 
9
- import {log} from '@luma.gl/core';
10
9
  import {type ParsedPBRMaterial} from '../pbr/pbr-material';
11
10
  import {type PBREnvironment} from '../pbr/pbr-environment';
12
11
  import {type PBRMaterialBindings} from '@luma.gl/shadertools';
12
+ import {GLEnum} from '../webgl-to-webgpu/gltf-webgl-constants';
13
13
  import {convertSampler} from '../webgl-to-webgpu/convert-webgl-sampler';
14
14
 
15
15
  // TODO - synchronize the GLTF... types with loaders.gl
@@ -19,6 +19,7 @@ import {convertSampler} from '../webgl-to-webgpu/convert-webgl-sampler';
19
19
 
20
20
  type GLTFTexture = {
21
21
  id: string;
22
+ index?: number;
22
23
  texture: {source: {image: any}; sampler: {parameters: any}};
23
24
  uniformName?: string;
24
25
  // is this on all textures?
@@ -36,16 +37,114 @@ type GLTFPBRMetallicRoughness = {
36
37
  };
37
38
 
38
39
  type GLTFPBRMaterial = {
40
+ extensions?: GLTFMaterialExtensions;
39
41
  unlit?: boolean;
40
42
  pbrMetallicRoughness?: GLTFPBRMetallicRoughness;
41
43
  normalTexture?: GLTFTexture;
42
44
  occlusionTexture?: GLTFTexture;
43
45
  emissiveTexture?: GLTFTexture;
44
46
  emissiveFactor?: [number, number, number];
45
- alphaMode?: 'MASK' | 'BLEND';
47
+ alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
48
+ doubleSided?: boolean;
46
49
  alphaCutoff?: number;
47
50
  };
48
51
 
52
+ type GLTFMaterialSpecularExtension = {
53
+ specularFactor?: number;
54
+ specularTexture?: GLTFTexture;
55
+ specularColorFactor?: [number, number, number];
56
+ specularColorTexture?: GLTFTexture;
57
+ };
58
+
59
+ type GLTFMaterialIorExtension = {
60
+ ior?: number;
61
+ };
62
+
63
+ type GLTFMaterialTransmissionExtension = {
64
+ transmissionFactor?: number;
65
+ transmissionTexture?: GLTFTexture;
66
+ };
67
+
68
+ type GLTFMaterialVolumeExtension = {
69
+ thicknessFactor?: number;
70
+ thicknessTexture?: GLTFTexture;
71
+ attenuationDistance?: number;
72
+ attenuationColor?: [number, number, number];
73
+ };
74
+
75
+ type GLTFMaterialClearcoatExtension = {
76
+ clearcoatFactor?: number;
77
+ clearcoatTexture?: GLTFTexture;
78
+ clearcoatRoughnessFactor?: number;
79
+ clearcoatRoughnessTexture?: GLTFTexture;
80
+ clearcoatNormalTexture?: GLTFTexture;
81
+ };
82
+
83
+ type GLTFMaterialSheenExtension = {
84
+ sheenColorFactor?: [number, number, number];
85
+ sheenColorTexture?: GLTFTexture;
86
+ sheenRoughnessFactor?: number;
87
+ sheenRoughnessTexture?: GLTFTexture;
88
+ };
89
+
90
+ type GLTFMaterialIridescenceExtension = {
91
+ iridescenceFactor?: number;
92
+ iridescenceTexture?: GLTFTexture;
93
+ iridescenceIor?: number;
94
+ iridescenceThicknessMinimum?: number;
95
+ iridescenceThicknessMaximum?: number;
96
+ iridescenceThicknessTexture?: GLTFTexture;
97
+ };
98
+
99
+ type GLTFMaterialAnisotropyExtension = {
100
+ anisotropyStrength?: number;
101
+ anisotropyRotation?: number;
102
+ anisotropyTexture?: GLTFTexture;
103
+ };
104
+
105
+ type GLTFMaterialEmissiveStrengthExtension = {
106
+ emissiveStrength?: number;
107
+ };
108
+
109
+ type GLTFMaterialExtensions = {
110
+ KHR_materials_unlit?: Record<string, never>;
111
+ KHR_materials_specular?: GLTFMaterialSpecularExtension;
112
+ KHR_materials_ior?: GLTFMaterialIorExtension;
113
+ KHR_materials_transmission?: GLTFMaterialTransmissionExtension;
114
+ KHR_materials_volume?: GLTFMaterialVolumeExtension;
115
+ KHR_materials_clearcoat?: GLTFMaterialClearcoatExtension;
116
+ KHR_materials_sheen?: GLTFMaterialSheenExtension;
117
+ KHR_materials_iridescence?: GLTFMaterialIridescenceExtension;
118
+ KHR_materials_anisotropy?: GLTFMaterialAnisotropyExtension;
119
+ KHR_materials_emissive_strength?: GLTFMaterialEmissiveStrengthExtension;
120
+ };
121
+
122
+ type TextureEnabledUniformName =
123
+ | 'baseColorMapEnabled'
124
+ | 'normalMapEnabled'
125
+ | 'emissiveMapEnabled'
126
+ | 'metallicRoughnessMapEnabled'
127
+ | 'occlusionMapEnabled'
128
+ | 'specularColorMapEnabled'
129
+ | 'specularIntensityMapEnabled'
130
+ | 'transmissionMapEnabled'
131
+ | 'clearcoatMapEnabled'
132
+ | 'clearcoatRoughnessMapEnabled'
133
+ | 'sheenColorMapEnabled'
134
+ | 'sheenRoughnessMapEnabled'
135
+ | 'iridescenceMapEnabled'
136
+ | 'anisotropyMapEnabled';
137
+
138
+ type TextureFeatureOptions = {
139
+ define?: string;
140
+ enabledUniformName?: TextureEnabledUniformName;
141
+ };
142
+
143
+ type TextureParseOptions = {
144
+ featureOptions?: TextureFeatureOptions;
145
+ gltf?: GLTFPostprocessed;
146
+ };
147
+
49
148
  export type ParsePBRMaterialOptions = {
50
149
  /** Debug PBR shader */
51
150
  pbrDebug?: boolean;
@@ -55,6 +154,10 @@ export type ParsePBRMaterialOptions = {
55
154
  useTangents?: boolean;
56
155
  /** provide an image based (texture cube) lighting environment */
57
156
  imageBasedLightingEnvironment?: PBREnvironment;
157
+ /** parent post-processed glTF, used to resolve extension texture infos */
158
+ gltf?: GLTFPostprocessed;
159
+ /** run primitive-attribute diagnostics such as missing TEXCOORD_0 / NORMAL */
160
+ validateAttributes?: boolean;
58
161
  };
59
162
 
60
163
  /**
@@ -93,7 +196,8 @@ export function parsePBRMaterial(
93
196
  imageBasedLightingEnvironment.diffuseEnvSampler.texture;
94
197
  parsedMaterial.bindings.pbr_specularEnvSampler =
95
198
  imageBasedLightingEnvironment.specularEnvSampler.texture;
96
- parsedMaterial.bindings.pbr_BrdfLUT = imageBasedLightingEnvironment.brdfLutTexture.texture;
199
+ parsedMaterial.bindings.pbr_brdfLUT = imageBasedLightingEnvironment.brdfLutTexture.texture;
200
+ parsedMaterial.uniforms.IBLenabled = true;
97
201
  parsedMaterial.uniforms.scaleIBLAmbient = [1, 1];
98
202
  }
99
203
 
@@ -107,112 +211,224 @@ export function parsePBRMaterial(
107
211
  if (attributes['NORMAL']) parsedMaterial.defines['HAS_NORMALS'] = true;
108
212
  if (attributes['TANGENT'] && options?.useTangents) parsedMaterial.defines['HAS_TANGENTS'] = true;
109
213
  if (attributes['TEXCOORD_0']) parsedMaterial.defines['HAS_UV'] = true;
214
+ if (attributes['JOINTS_0'] && attributes['WEIGHTS_0']) parsedMaterial.defines['HAS_SKIN'] = true;
110
215
  if (attributes['COLOR_0']) parsedMaterial.defines['HAS_COLORS'] = true;
111
216
 
112
217
  if (options?.imageBasedLightingEnvironment) parsedMaterial.defines['USE_IBL'] = true;
113
218
  if (options?.lights) parsedMaterial.defines['USE_LIGHTS'] = true;
114
219
 
115
220
  if (material) {
116
- parseMaterial(device, material, parsedMaterial);
221
+ if (options.validateAttributes !== false) {
222
+ warnOnMissingExpectedAttributes(material, attributes);
223
+ }
224
+ parseMaterial(device, material, parsedMaterial, options.gltf);
117
225
  }
118
226
 
119
227
  return parsedMaterial;
120
228
  }
121
229
 
230
+ function warnOnMissingExpectedAttributes(
231
+ material: GLTFPBRMaterial,
232
+ attributes: Record<string, any>
233
+ ): void {
234
+ const uvDependentTextureSlots = getUvDependentTextureSlots(material);
235
+ if (uvDependentTextureSlots.length > 0 && !attributes['TEXCOORD_0']) {
236
+ log.warn(
237
+ `glTF material uses ${uvDependentTextureSlots.join(', ')} but primitive is missing TEXCOORD_0; textured shading will sample the default UV coordinates`
238
+ )();
239
+ }
240
+
241
+ const isUnlitMaterial = Boolean(material.unlit || material.extensions?.KHR_materials_unlit);
242
+ if (isUnlitMaterial || attributes['NORMAL']) {
243
+ return;
244
+ }
245
+
246
+ const missingNormalReason = material.normalTexture
247
+ ? 'lit PBR shading with normalTexture'
248
+ : 'lit PBR shading';
249
+ log.warn(
250
+ `glTF primitive is missing NORMAL while using ${missingNormalReason}; shading will fall back to geometric normals`
251
+ )();
252
+ }
253
+
254
+ function getUvDependentTextureSlots(material: GLTFPBRMaterial): string[] {
255
+ const uvDependentTextureSlots: string[] = [];
256
+
257
+ if (material.pbrMetallicRoughness?.baseColorTexture) {
258
+ uvDependentTextureSlots.push('baseColorTexture');
259
+ }
260
+ if (material.pbrMetallicRoughness?.metallicRoughnessTexture) {
261
+ uvDependentTextureSlots.push('metallicRoughnessTexture');
262
+ }
263
+ if (material.normalTexture) {
264
+ uvDependentTextureSlots.push('normalTexture');
265
+ }
266
+ if (material.occlusionTexture) {
267
+ uvDependentTextureSlots.push('occlusionTexture');
268
+ }
269
+ if (material.emissiveTexture) {
270
+ uvDependentTextureSlots.push('emissiveTexture');
271
+ }
272
+ if (material.extensions?.KHR_materials_specular?.specularTexture) {
273
+ uvDependentTextureSlots.push('KHR_materials_specular.specularTexture');
274
+ }
275
+ if (material.extensions?.KHR_materials_specular?.specularColorTexture) {
276
+ uvDependentTextureSlots.push('KHR_materials_specular.specularColorTexture');
277
+ }
278
+ if (material.extensions?.KHR_materials_transmission?.transmissionTexture) {
279
+ uvDependentTextureSlots.push('KHR_materials_transmission.transmissionTexture');
280
+ }
281
+ if (material.extensions?.KHR_materials_clearcoat?.clearcoatTexture) {
282
+ uvDependentTextureSlots.push('KHR_materials_clearcoat.clearcoatTexture');
283
+ }
284
+ if (material.extensions?.KHR_materials_clearcoat?.clearcoatRoughnessTexture) {
285
+ uvDependentTextureSlots.push('KHR_materials_clearcoat.clearcoatRoughnessTexture');
286
+ }
287
+ if (material.extensions?.KHR_materials_sheen?.sheenColorTexture) {
288
+ uvDependentTextureSlots.push('KHR_materials_sheen.sheenColorTexture');
289
+ }
290
+ if (material.extensions?.KHR_materials_sheen?.sheenRoughnessTexture) {
291
+ uvDependentTextureSlots.push('KHR_materials_sheen.sheenRoughnessTexture');
292
+ }
293
+ if (material.extensions?.KHR_materials_iridescence?.iridescenceTexture) {
294
+ uvDependentTextureSlots.push('KHR_materials_iridescence.iridescenceTexture');
295
+ }
296
+ if (material.extensions?.KHR_materials_anisotropy?.anisotropyTexture) {
297
+ uvDependentTextureSlots.push('KHR_materials_anisotropy.anisotropyTexture');
298
+ }
299
+
300
+ return uvDependentTextureSlots;
301
+ }
302
+
122
303
  /** Parse GLTF material record */
123
304
  function parseMaterial(
124
305
  device: Device,
125
306
  material: GLTFPBRMaterial,
126
- parsedMaterial: ParsedPBRMaterial
307
+ parsedMaterial: ParsedPBRMaterial,
308
+ gltf?: GLTFPostprocessed
127
309
  ): void {
128
- parsedMaterial.uniforms.unlit = Boolean(material.unlit);
310
+ parsedMaterial.uniforms.unlit = Boolean(
311
+ material.unlit || material.extensions?.KHR_materials_unlit
312
+ );
129
313
 
130
314
  if (material.pbrMetallicRoughness) {
131
- parsePbrMetallicRoughness(device, material.pbrMetallicRoughness, parsedMaterial);
315
+ parsePbrMetallicRoughness(device, material.pbrMetallicRoughness, parsedMaterial, gltf);
132
316
  }
133
317
  if (material.normalTexture) {
134
- addTexture(
135
- device,
136
- material.normalTexture,
137
- 'pbr_normalSampler',
138
- 'HAS_NORMALMAP',
139
- parsedMaterial
140
- );
318
+ addTexture(device, material.normalTexture, 'pbr_normalSampler', parsedMaterial, {
319
+ featureOptions: {
320
+ define: 'HAS_NORMALMAP',
321
+ enabledUniformName: 'normalMapEnabled'
322
+ },
323
+ gltf
324
+ });
141
325
 
142
326
  const {scale = 1} = material.normalTexture;
143
327
  parsedMaterial.uniforms.normalScale = scale;
144
328
  }
145
329
  if (material.occlusionTexture) {
146
- addTexture(
147
- device,
148
- material.occlusionTexture,
149
- 'pbr_occlusionSampler',
150
- 'HAS_OCCLUSIONMAP',
151
- parsedMaterial
152
- );
330
+ addTexture(device, material.occlusionTexture, 'pbr_occlusionSampler', parsedMaterial, {
331
+ featureOptions: {
332
+ define: 'HAS_OCCLUSIONMAP',
333
+ enabledUniformName: 'occlusionMapEnabled'
334
+ },
335
+ gltf
336
+ });
153
337
 
154
338
  const {strength = 1} = material.occlusionTexture;
155
339
  parsedMaterial.uniforms.occlusionStrength = strength;
156
340
  }
341
+ parsedMaterial.uniforms.emissiveFactor = material.emissiveFactor || [0, 0, 0];
157
342
  if (material.emissiveTexture) {
158
- addTexture(
159
- device,
160
- material.emissiveTexture,
161
- 'pbr_emissiveSampler',
162
- 'HAS_EMISSIVEMAP',
163
- parsedMaterial
164
- );
165
- parsedMaterial.uniforms.emissiveFactor = material.emissiveFactor || [0, 0, 0];
343
+ addTexture(device, material.emissiveTexture, 'pbr_emissiveSampler', parsedMaterial, {
344
+ featureOptions: {
345
+ define: 'HAS_EMISSIVEMAP',
346
+ enabledUniformName: 'emissiveMapEnabled'
347
+ },
348
+ gltf
349
+ });
166
350
  }
167
351
 
168
- switch (material.alphaMode || 'MASK') {
169
- case 'MASK':
352
+ parseMaterialExtensions(device, material.extensions, parsedMaterial, gltf);
353
+
354
+ switch (material.alphaMode || 'OPAQUE') {
355
+ case 'OPAQUE':
356
+ break;
357
+ case 'MASK': {
170
358
  const {alphaCutoff = 0.5} = material;
171
359
  parsedMaterial.defines['ALPHA_CUTOFF'] = true;
360
+ parsedMaterial.uniforms.alphaCutoffEnabled = true;
172
361
  parsedMaterial.uniforms.alphaCutoff = alphaCutoff;
173
362
  break;
363
+ }
174
364
  case 'BLEND':
175
365
  log.warn('glTF BLEND alphaMode might not work well because it requires mesh sorting')();
366
+ applyAlphaBlendParameters(parsedMaterial);
176
367
 
177
- // WebGPU style parameters
178
- parsedMaterial.parameters.blend = true;
368
+ break;
369
+ }
370
+ }
179
371
 
180
- parsedMaterial.parameters.blendColorOperation = 'add';
181
- parsedMaterial.parameters.blendColorSrcFactor = 'src-alpha';
182
- parsedMaterial.parameters.blendColorDstFactor = 'one-minus-src-alpha';
372
+ function applyAlphaBlendParameters(parsedMaterial: ParsedPBRMaterial): void {
373
+ parsedMaterial.parameters.blend = true;
374
+ parsedMaterial.parameters.blendColorOperation = 'add';
375
+ parsedMaterial.parameters.blendColorSrcFactor = 'src-alpha';
376
+ parsedMaterial.parameters.blendColorDstFactor = 'one-minus-src-alpha';
377
+ parsedMaterial.parameters.blendAlphaOperation = 'add';
378
+ parsedMaterial.parameters.blendAlphaSrcFactor = 'one';
379
+ parsedMaterial.parameters.blendAlphaDstFactor = 'one-minus-src-alpha';
183
380
 
184
- parsedMaterial.parameters.blendAlphaOperation = 'add';
185
- parsedMaterial.parameters.blendAlphaSrcFactor = 'one';
186
- parsedMaterial.parameters.blendAlphaDstFactor = 'one-minus-src-alpha';
381
+ parsedMaterial.glParameters['blend'] = true;
382
+ parsedMaterial.glParameters['blendEquation'] = GLEnum.FUNC_ADD;
383
+ parsedMaterial.glParameters['blendFunc'] = [
384
+ GLEnum.SRC_ALPHA,
385
+ GLEnum.ONE_MINUS_SRC_ALPHA,
386
+ GLEnum.ONE,
387
+ GLEnum.ONE_MINUS_SRC_ALPHA
388
+ ];
389
+ }
187
390
 
188
- // GL parameters
189
- // TODO - remove in favor of parameters
190
- parsedMaterial.glParameters['blend'] = true;
191
- parsedMaterial.glParameters['blendEquation'] = GL.FUNC_ADD;
192
- parsedMaterial.glParameters['blendFunc'] = [
193
- GL.SRC_ALPHA,
194
- GL.ONE_MINUS_SRC_ALPHA,
195
- GL.ONE,
196
- GL.ONE_MINUS_SRC_ALPHA
197
- ];
391
+ function applyTransmissionBlendApproximation(parsedMaterial: ParsedPBRMaterial): void {
392
+ parsedMaterial.parameters.blend = true;
393
+ parsedMaterial.parameters.depthWriteEnabled = false;
394
+ parsedMaterial.parameters.blendColorOperation = 'add';
395
+ parsedMaterial.parameters.blendColorSrcFactor = 'one';
396
+ parsedMaterial.parameters.blendColorDstFactor = 'one-minus-src-alpha';
397
+ parsedMaterial.parameters.blendAlphaOperation = 'add';
398
+ parsedMaterial.parameters.blendAlphaSrcFactor = 'one';
399
+ parsedMaterial.parameters.blendAlphaDstFactor = 'one-minus-src-alpha';
198
400
 
199
- break;
200
- }
401
+ parsedMaterial.glParameters['blend'] = true;
402
+ parsedMaterial.glParameters['depthMask'] = false;
403
+ parsedMaterial.glParameters['blendEquation'] = GLEnum.FUNC_ADD;
404
+ parsedMaterial.glParameters['blendFunc'] = [
405
+ GLEnum.ONE,
406
+ GLEnum.ONE_MINUS_SRC_ALPHA,
407
+ GLEnum.ONE,
408
+ GLEnum.ONE_MINUS_SRC_ALPHA
409
+ ];
201
410
  }
202
411
 
203
412
  /** Parse GLTF material sub record */
204
413
  function parsePbrMetallicRoughness(
205
414
  device: Device,
206
415
  pbrMetallicRoughness: GLTFPBRMetallicRoughness,
207
- parsedMaterial: ParsedPBRMaterial
416
+ parsedMaterial: ParsedPBRMaterial,
417
+ gltf?: GLTFPostprocessed
208
418
  ): void {
209
419
  if (pbrMetallicRoughness.baseColorTexture) {
210
420
  addTexture(
211
421
  device,
212
422
  pbrMetallicRoughness.baseColorTexture,
213
423
  'pbr_baseColorSampler',
214
- 'HAS_BASECOLORMAP',
215
- parsedMaterial
424
+ parsedMaterial,
425
+ {
426
+ featureOptions: {
427
+ define: 'HAS_BASECOLORMAP',
428
+ enabledUniformName: 'baseColorMapEnabled'
429
+ },
430
+ gltf
431
+ }
216
432
  );
217
433
  }
218
434
  parsedMaterial.uniforms.baseColorFactor = pbrMetallicRoughness.baseColorFactor || [1, 1, 1, 1];
@@ -222,30 +438,362 @@ function parsePbrMetallicRoughness(
222
438
  device,
223
439
  pbrMetallicRoughness.metallicRoughnessTexture,
224
440
  'pbr_metallicRoughnessSampler',
225
- 'HAS_METALROUGHNESSMAP',
226
- parsedMaterial
441
+ parsedMaterial,
442
+ {
443
+ featureOptions: {
444
+ define: 'HAS_METALROUGHNESSMAP',
445
+ enabledUniformName: 'metallicRoughnessMapEnabled'
446
+ },
447
+ gltf
448
+ }
227
449
  );
228
450
  }
229
451
  const {metallicFactor = 1, roughnessFactor = 1} = pbrMetallicRoughness;
230
452
  parsedMaterial.uniforms.metallicRoughnessValues = [metallicFactor, roughnessFactor];
231
453
  }
232
454
 
455
+ function parseMaterialExtensions(
456
+ device: Device,
457
+ extensions: GLTFMaterialExtensions | undefined,
458
+ parsedMaterial: ParsedPBRMaterial,
459
+ gltf?: GLTFPostprocessed
460
+ ): void {
461
+ if (!extensions) {
462
+ return;
463
+ }
464
+
465
+ if (hasMaterialExtensionShading(extensions)) {
466
+ parsedMaterial.defines['USE_MATERIAL_EXTENSIONS'] = true;
467
+ }
468
+
469
+ parseSpecularExtension(device, extensions.KHR_materials_specular, parsedMaterial, gltf);
470
+ parseIorExtension(extensions.KHR_materials_ior, parsedMaterial);
471
+ parseTransmissionExtension(device, extensions.KHR_materials_transmission, parsedMaterial, gltf);
472
+ parseVolumeExtension(device, extensions.KHR_materials_volume, parsedMaterial, gltf);
473
+ parseClearcoatExtension(device, extensions.KHR_materials_clearcoat, parsedMaterial, gltf);
474
+ parseSheenExtension(device, extensions.KHR_materials_sheen, parsedMaterial, gltf);
475
+ parseIridescenceExtension(device, extensions.KHR_materials_iridescence, parsedMaterial, gltf);
476
+ parseAnisotropyExtension(device, extensions.KHR_materials_anisotropy, parsedMaterial, gltf);
477
+ parseEmissiveStrengthExtension(extensions.KHR_materials_emissive_strength, parsedMaterial);
478
+ }
479
+
480
+ function hasMaterialExtensionShading(extensions: GLTFMaterialExtensions): boolean {
481
+ return Boolean(
482
+ extensions.KHR_materials_specular ||
483
+ extensions.KHR_materials_ior ||
484
+ extensions.KHR_materials_transmission ||
485
+ extensions.KHR_materials_volume ||
486
+ extensions.KHR_materials_clearcoat ||
487
+ extensions.KHR_materials_sheen ||
488
+ extensions.KHR_materials_iridescence ||
489
+ extensions.KHR_materials_anisotropy
490
+ );
491
+ }
492
+
493
+ function parseSpecularExtension(
494
+ device: Device,
495
+ extension: GLTFMaterialSpecularExtension | undefined,
496
+ parsedMaterial: ParsedPBRMaterial,
497
+ gltf?: GLTFPostprocessed
498
+ ): void {
499
+ if (!extension) {
500
+ return;
501
+ }
502
+
503
+ if (extension.specularColorFactor) {
504
+ parsedMaterial.uniforms.specularColorFactor = extension.specularColorFactor;
505
+ }
506
+ if (extension.specularFactor !== undefined) {
507
+ parsedMaterial.uniforms.specularIntensityFactor = extension.specularFactor;
508
+ }
509
+ if (extension.specularColorTexture) {
510
+ addTexture(device, extension.specularColorTexture, 'pbr_specularColorSampler', parsedMaterial, {
511
+ featureOptions: {
512
+ define: 'HAS_SPECULARCOLORMAP',
513
+ enabledUniformName: 'specularColorMapEnabled'
514
+ },
515
+ gltf
516
+ });
517
+ }
518
+ if (extension.specularTexture) {
519
+ addTexture(device, extension.specularTexture, 'pbr_specularIntensitySampler', parsedMaterial, {
520
+ featureOptions: {
521
+ define: 'HAS_SPECULARINTENSITYMAP',
522
+ enabledUniformName: 'specularIntensityMapEnabled'
523
+ },
524
+ gltf
525
+ });
526
+ }
527
+ }
528
+
529
+ function parseIorExtension(
530
+ extension: GLTFMaterialIorExtension | undefined,
531
+ parsedMaterial: ParsedPBRMaterial
532
+ ): void {
533
+ if (extension?.ior !== undefined) {
534
+ parsedMaterial.uniforms.ior = extension.ior;
535
+ }
536
+ }
537
+
538
+ function parseTransmissionExtension(
539
+ device: Device,
540
+ extension: GLTFMaterialTransmissionExtension | undefined,
541
+ parsedMaterial: ParsedPBRMaterial,
542
+ gltf?: GLTFPostprocessed
543
+ ): void {
544
+ if (!extension) {
545
+ return;
546
+ }
547
+
548
+ if (extension.transmissionFactor !== undefined) {
549
+ parsedMaterial.uniforms.transmissionFactor = extension.transmissionFactor;
550
+ }
551
+ if (extension.transmissionTexture) {
552
+ addTexture(device, extension.transmissionTexture, 'pbr_transmissionSampler', parsedMaterial, {
553
+ featureOptions: {
554
+ define: 'HAS_TRANSMISSIONMAP',
555
+ enabledUniformName: 'transmissionMapEnabled'
556
+ },
557
+ gltf
558
+ });
559
+ }
560
+
561
+ if ((extension.transmissionFactor ?? 0) > 0 || extension.transmissionTexture) {
562
+ log.warn(
563
+ 'KHR_materials_transmission uses a premultiplied-alpha blending approximation and may require mesh sorting'
564
+ )();
565
+ applyTransmissionBlendApproximation(parsedMaterial);
566
+ }
567
+ }
568
+
569
+ function parseVolumeExtension(
570
+ device: Device,
571
+ extension: GLTFMaterialVolumeExtension | undefined,
572
+ parsedMaterial: ParsedPBRMaterial,
573
+ gltf?: GLTFPostprocessed
574
+ ): void {
575
+ if (!extension) {
576
+ return;
577
+ }
578
+
579
+ if (extension.thicknessFactor !== undefined) {
580
+ parsedMaterial.uniforms.thicknessFactor = extension.thicknessFactor;
581
+ }
582
+ if (extension.thicknessTexture) {
583
+ addTexture(device, extension.thicknessTexture, 'pbr_thicknessSampler', parsedMaterial, {
584
+ featureOptions: {
585
+ define: 'HAS_THICKNESSMAP'
586
+ },
587
+ gltf
588
+ });
589
+ }
590
+ if (extension.attenuationDistance !== undefined) {
591
+ parsedMaterial.uniforms.attenuationDistance = extension.attenuationDistance;
592
+ }
593
+ if (extension.attenuationColor) {
594
+ parsedMaterial.uniforms.attenuationColor = extension.attenuationColor;
595
+ }
596
+ }
597
+
598
+ function parseClearcoatExtension(
599
+ device: Device,
600
+ extension: GLTFMaterialClearcoatExtension | undefined,
601
+ parsedMaterial: ParsedPBRMaterial,
602
+ gltf?: GLTFPostprocessed
603
+ ): void {
604
+ if (!extension) {
605
+ return;
606
+ }
607
+
608
+ if (extension.clearcoatFactor !== undefined) {
609
+ parsedMaterial.uniforms.clearcoatFactor = extension.clearcoatFactor;
610
+ }
611
+ if (extension.clearcoatRoughnessFactor !== undefined) {
612
+ parsedMaterial.uniforms.clearcoatRoughnessFactor = extension.clearcoatRoughnessFactor;
613
+ }
614
+ if (extension.clearcoatTexture) {
615
+ addTexture(device, extension.clearcoatTexture, 'pbr_clearcoatSampler', parsedMaterial, {
616
+ featureOptions: {
617
+ define: 'HAS_CLEARCOATMAP',
618
+ enabledUniformName: 'clearcoatMapEnabled'
619
+ },
620
+ gltf
621
+ });
622
+ }
623
+ if (extension.clearcoatRoughnessTexture) {
624
+ addTexture(
625
+ device,
626
+ extension.clearcoatRoughnessTexture,
627
+ 'pbr_clearcoatRoughnessSampler',
628
+ parsedMaterial,
629
+ {
630
+ featureOptions: {
631
+ define: 'HAS_CLEARCOATROUGHNESSMAP',
632
+ enabledUniformName: 'clearcoatRoughnessMapEnabled'
633
+ },
634
+ gltf
635
+ }
636
+ );
637
+ }
638
+ if (extension.clearcoatNormalTexture) {
639
+ addTexture(
640
+ device,
641
+ extension.clearcoatNormalTexture,
642
+ 'pbr_clearcoatNormalSampler',
643
+ parsedMaterial,
644
+ {
645
+ featureOptions: {
646
+ define: 'HAS_CLEARCOATNORMALMAP'
647
+ },
648
+ gltf
649
+ }
650
+ );
651
+ }
652
+ }
653
+
654
+ function parseSheenExtension(
655
+ device: Device,
656
+ extension: GLTFMaterialSheenExtension | undefined,
657
+ parsedMaterial: ParsedPBRMaterial,
658
+ gltf?: GLTFPostprocessed
659
+ ): void {
660
+ if (!extension) {
661
+ return;
662
+ }
663
+
664
+ if (extension.sheenColorFactor) {
665
+ parsedMaterial.uniforms.sheenColorFactor = extension.sheenColorFactor;
666
+ }
667
+ if (extension.sheenRoughnessFactor !== undefined) {
668
+ parsedMaterial.uniforms.sheenRoughnessFactor = extension.sheenRoughnessFactor;
669
+ }
670
+ if (extension.sheenColorTexture) {
671
+ addTexture(device, extension.sheenColorTexture, 'pbr_sheenColorSampler', parsedMaterial, {
672
+ featureOptions: {
673
+ define: 'HAS_SHEENCOLORMAP',
674
+ enabledUniformName: 'sheenColorMapEnabled'
675
+ },
676
+ gltf
677
+ });
678
+ }
679
+ if (extension.sheenRoughnessTexture) {
680
+ addTexture(
681
+ device,
682
+ extension.sheenRoughnessTexture,
683
+ 'pbr_sheenRoughnessSampler',
684
+ parsedMaterial,
685
+ {
686
+ featureOptions: {
687
+ define: 'HAS_SHEENROUGHNESSMAP',
688
+ enabledUniformName: 'sheenRoughnessMapEnabled'
689
+ },
690
+ gltf
691
+ }
692
+ );
693
+ }
694
+ }
695
+
696
+ function parseIridescenceExtension(
697
+ device: Device,
698
+ extension: GLTFMaterialIridescenceExtension | undefined,
699
+ parsedMaterial: ParsedPBRMaterial,
700
+ gltf?: GLTFPostprocessed
701
+ ): void {
702
+ if (!extension) {
703
+ return;
704
+ }
705
+
706
+ if (extension.iridescenceFactor !== undefined) {
707
+ parsedMaterial.uniforms.iridescenceFactor = extension.iridescenceFactor;
708
+ }
709
+ if (extension.iridescenceIor !== undefined) {
710
+ parsedMaterial.uniforms.iridescenceIor = extension.iridescenceIor;
711
+ }
712
+ if (
713
+ extension.iridescenceThicknessMinimum !== undefined ||
714
+ extension.iridescenceThicknessMaximum !== undefined
715
+ ) {
716
+ parsedMaterial.uniforms.iridescenceThicknessRange = [
717
+ extension.iridescenceThicknessMinimum ?? 100,
718
+ extension.iridescenceThicknessMaximum ?? 400
719
+ ];
720
+ }
721
+ if (extension.iridescenceTexture) {
722
+ addTexture(device, extension.iridescenceTexture, 'pbr_iridescenceSampler', parsedMaterial, {
723
+ featureOptions: {
724
+ define: 'HAS_IRIDESCENCEMAP',
725
+ enabledUniformName: 'iridescenceMapEnabled'
726
+ },
727
+ gltf
728
+ });
729
+ }
730
+ if (extension.iridescenceThicknessTexture) {
731
+ addTexture(
732
+ device,
733
+ extension.iridescenceThicknessTexture,
734
+ 'pbr_iridescenceThicknessSampler',
735
+ parsedMaterial,
736
+ {
737
+ featureOptions: {
738
+ define: 'HAS_IRIDESCENCETHICKNESSMAP'
739
+ },
740
+ gltf
741
+ }
742
+ );
743
+ }
744
+ }
745
+
746
+ function parseAnisotropyExtension(
747
+ device: Device,
748
+ extension: GLTFMaterialAnisotropyExtension | undefined,
749
+ parsedMaterial: ParsedPBRMaterial,
750
+ gltf?: GLTFPostprocessed
751
+ ): void {
752
+ if (!extension) {
753
+ return;
754
+ }
755
+
756
+ if (extension.anisotropyStrength !== undefined) {
757
+ parsedMaterial.uniforms.anisotropyStrength = extension.anisotropyStrength;
758
+ }
759
+ if (extension.anisotropyRotation !== undefined) {
760
+ parsedMaterial.uniforms.anisotropyRotation = extension.anisotropyRotation;
761
+ }
762
+ if (extension.anisotropyTexture) {
763
+ addTexture(device, extension.anisotropyTexture, 'pbr_anisotropySampler', parsedMaterial, {
764
+ featureOptions: {
765
+ define: 'HAS_ANISOTROPYMAP',
766
+ enabledUniformName: 'anisotropyMapEnabled'
767
+ },
768
+ gltf
769
+ });
770
+ }
771
+ }
772
+
773
+ function parseEmissiveStrengthExtension(
774
+ extension: GLTFMaterialEmissiveStrengthExtension | undefined,
775
+ parsedMaterial: ParsedPBRMaterial
776
+ ): void {
777
+ if (extension?.emissiveStrength !== undefined) {
778
+ parsedMaterial.uniforms.emissiveStrength = extension.emissiveStrength;
779
+ }
780
+ }
781
+
233
782
  /** Create a texture from a glTF texture/sampler/image combo and add it to bindings */
234
783
  function addTexture(
235
784
  device: Device,
236
785
  gltfTexture: GLTFTexture,
237
786
  uniformName: keyof PBRMaterialBindings,
238
- define: string,
239
- parsedMaterial: ParsedPBRMaterial
787
+ parsedMaterial: ParsedPBRMaterial,
788
+ textureParseOptions: TextureParseOptions = {}
240
789
  ): void {
241
- const image = gltfTexture.texture.source.image;
242
- let textureOptions;
243
-
244
- if (image.compressed) {
245
- textureOptions = image;
246
- } else {
247
- // Texture2D accepts a promise that returns an image as data (Async Textures)
248
- textureOptions = {data: image};
790
+ const {featureOptions = {}, gltf} = textureParseOptions;
791
+ const {define, enabledUniformName} = featureOptions;
792
+ const resolvedTextureInfo = resolveTextureInfo(gltfTexture, gltf);
793
+ const image = resolvedTextureInfo.texture?.source?.image;
794
+ if (!image) {
795
+ log.warn(`Skipping unresolved glTF texture for ${String(uniformName)}`)();
796
+ return;
249
797
  }
250
798
 
251
799
  const gltfSampler = {
@@ -253,20 +801,251 @@ function addTexture(
253
801
  wrapT: 10497, // default REPEAT T (V) wrapping mode.
254
802
  minFilter: 9729, // default LINEAR filtering
255
803
  magFilter: 9729, // default LINEAR filtering
256
- ...gltfTexture?.texture?.sampler
804
+ ...resolvedTextureInfo?.texture?.sampler
257
805
  } as GLTFSampler;
258
806
 
259
- const texture: Texture = device.createTexture({
260
- id: gltfTexture.uniformName || gltfTexture.id,
261
- sampler: convertSampler(gltfSampler),
262
- ...textureOptions
263
- });
807
+ const baseOptions = {
808
+ id: resolvedTextureInfo.uniformName || resolvedTextureInfo.id,
809
+ sampler: convertSampler(gltfSampler)
810
+ };
811
+
812
+ let texture: Texture;
813
+
814
+ if (image.compressed) {
815
+ texture = createCompressedTexture(device, image, baseOptions);
816
+ } else {
817
+ const {width, height} = device.getExternalImageSize(image);
818
+ texture = device.createTexture({
819
+ ...baseOptions,
820
+ width,
821
+ height,
822
+ data: image
823
+ });
824
+ }
264
825
 
265
826
  parsedMaterial.bindings[uniformName] = texture;
266
827
  if (define) parsedMaterial.defines[define] = true;
828
+ if (enabledUniformName) {
829
+ parsedMaterial.uniforms[enabledUniformName] = true;
830
+ }
267
831
  parsedMaterial.generatedTextures.push(texture);
268
832
  }
269
833
 
834
+ function resolveTextureInfo(gltfTexture: GLTFTexture, gltf?: GLTFPostprocessed): GLTFTexture {
835
+ if (gltfTexture.texture || gltfTexture.index === undefined || !gltf?.textures) {
836
+ return gltfTexture;
837
+ }
838
+
839
+ const resolvedTextureEntry = gltf.textures[gltfTexture.index] as
840
+ | Partial<GLTFTexture>
841
+ | GLTFTexture['texture']
842
+ | undefined;
843
+ if (!resolvedTextureEntry) {
844
+ return gltfTexture;
845
+ }
846
+
847
+ if ('texture' in resolvedTextureEntry && resolvedTextureEntry.texture) {
848
+ return {
849
+ ...resolvedTextureEntry,
850
+ ...gltfTexture,
851
+ texture: resolvedTextureEntry.texture
852
+ } as GLTFTexture;
853
+ }
854
+
855
+ if (!('source' in resolvedTextureEntry)) {
856
+ return gltfTexture;
857
+ }
858
+
859
+ return {
860
+ ...gltfTexture,
861
+ texture: resolvedTextureEntry
862
+ };
863
+ }
864
+
865
+ /** One mip level as produced by loaders.gl compressed texture parsers */
866
+ export type CompressedMipLevel = {
867
+ data: TypedArray;
868
+ width: number;
869
+ height: number;
870
+ textureFormat?: TextureFormat;
871
+ };
872
+
873
+ /**
874
+ * Compressed image from current loaders.gl releases.
875
+ * - `mipmaps` is a boolean (true), NOT an array
876
+ * - `data` is an Array of TextureLevel-like objects
877
+ * - Per-level `textureFormat` is already a luma.gl TextureFormat
878
+ * - Top-level `width`/`height` may be undefined
879
+ */
880
+ export type CompressedImageDataArray = {
881
+ compressed: true;
882
+ mipmaps?: boolean;
883
+ width?: number;
884
+ height?: number;
885
+ data: CompressedMipLevel[];
886
+ };
887
+
888
+ /**
889
+ * Hypothetical future format where `mipmaps` is an actual array.
890
+ * Kept for forward compatibility.
891
+ */
892
+ export type CompressedImageMipmapArray = {
893
+ compressed: true;
894
+ width?: number;
895
+ height?: number;
896
+ mipmaps: CompressedMipLevel[];
897
+ };
898
+
899
+ /** Union of all known loaders.gl compressed image shapes */
900
+ export type CompressedImage = CompressedImageDataArray | CompressedImageMipmapArray;
901
+
902
+ function createCompressedTextureFallback(
903
+ device: Device,
904
+ baseOptions: {id: string; sampler: SamplerProps}
905
+ ): Texture {
906
+ return device.createTexture({
907
+ ...baseOptions,
908
+ format: 'rgba8unorm',
909
+ width: 1,
910
+ height: 1,
911
+ mipLevels: 1
912
+ });
913
+ }
914
+
915
+ function resolveCompressedTextureFormat(level: CompressedMipLevel): TextureFormat | undefined {
916
+ return level.textureFormat;
917
+ }
918
+
919
+ /**
920
+ * Maximum mip levels that can be filled for a compressed texture.
921
+ * texStorage2D allocates level i at (baseW >> i) × (baseH >> i).
922
+ * Compressed formats can't upload data for levels smaller than one block,
923
+ * so we stop before either dimension drops below the block size.
924
+ */
925
+ function getMaxCompressedMipLevels(
926
+ baseWidth: number,
927
+ baseHeight: number,
928
+ format: TextureFormat
929
+ ): number {
930
+ const {blockWidth = 1, blockHeight = 1} = textureFormatDecoder.getInfo(format);
931
+ let count = 1;
932
+ for (let i = 1; ; i++) {
933
+ const w = Math.max(1, baseWidth >> i);
934
+ const h = Math.max(1, baseHeight >> i);
935
+ if (w < blockWidth || h < blockHeight) break;
936
+ count++;
937
+ }
938
+ return count;
939
+ }
940
+
941
+ /**
942
+ * Create a texture from compressed image data produced by loaders.gl.
943
+ * Handles current loaders.gl compressed image layouts:
944
+ *
945
+ * current: {compressed, mipmaps: true, data: [{data, width, height, textureFormat}, ...]}
946
+ * forward: {compressed, mipmaps: [{data, width, height, textureFormat}, ...]}
947
+ */
948
+ export function createCompressedTexture(
949
+ device: Device,
950
+ image: CompressedImage,
951
+ baseOptions: {id: string; sampler: SamplerProps}
952
+ ): Texture {
953
+ // Normalize mip levels from all known loaders.gl formats
954
+ let levels: CompressedMipLevel[];
955
+
956
+ if (Array.isArray((image as any).data) && (image as any).data[0]?.data) {
957
+ // loaders.gl current format: image.data is Array of mip-level objects
958
+ levels = (image as CompressedImageDataArray).data;
959
+ } else if ('mipmaps' in image && Array.isArray((image as CompressedImageMipmapArray).mipmaps)) {
960
+ // Hypothetical future format: image.mipmaps is an Array
961
+ levels = (image as CompressedImageMipmapArray).mipmaps;
962
+ } else {
963
+ levels = [];
964
+ }
965
+
966
+ if (levels.length === 0 || !levels[0]?.data) {
967
+ log.warn(
968
+ 'createCompressedTexture: compressed image has no valid mip levels, creating fallback'
969
+ )();
970
+ return createCompressedTextureFallback(device, baseOptions);
971
+ }
972
+
973
+ const baseLevel = levels[0];
974
+ const baseWidth = baseLevel.width ?? (image as any).width ?? 0;
975
+ const baseHeight = baseLevel.height ?? (image as any).height ?? 0;
976
+
977
+ if (baseWidth <= 0 || baseHeight <= 0) {
978
+ log.warn('createCompressedTexture: base level has invalid dimensions, creating fallback')();
979
+ return createCompressedTextureFallback(device, baseOptions);
980
+ }
981
+
982
+ const format = resolveCompressedTextureFormat(baseLevel);
983
+
984
+ if (!format) {
985
+ log.warn('createCompressedTexture: compressed image has no textureFormat, creating fallback')();
986
+ return createCompressedTextureFallback(device, baseOptions);
987
+ }
988
+
989
+ // Validate mip levels: truncate chain at first invalid level.
990
+ // Levels must be contiguous, so we stop at the first level that has
991
+ // a format mismatch, missing data, non-positive dimensions, or
992
+ // dimensions that don't match what texStorage2D will allocate.
993
+ //
994
+ // For block-compressed formats (ASTC, BC, ETC2), texStorage2D allocates
995
+ // mip levels down to 1×1 texels, but compressed data can't be smaller
996
+ // than one block (e.g. 4×4 for ASTC-4x4). Cap the chain so we never
997
+ // try to upload data whose block-aligned size exceeds the allocated level.
998
+ const maxMipLevels = getMaxCompressedMipLevels(baseWidth, baseHeight, format);
999
+ const levelLimit = Math.min(levels.length, maxMipLevels);
1000
+
1001
+ let validLevelCount = 1;
1002
+ for (let i = 1; i < levelLimit; i++) {
1003
+ const level = levels[i];
1004
+ if (!level.data || level.width <= 0 || level.height <= 0) {
1005
+ log.warn(`createCompressedTexture: mip level ${i} has invalid data/dimensions, truncating`)();
1006
+ break;
1007
+ }
1008
+ const levelFormat = resolveCompressedTextureFormat(level);
1009
+ if (levelFormat && levelFormat !== format) {
1010
+ log.warn(
1011
+ `createCompressedTexture: mip level ${i} format '${levelFormat}' differs from base '${format}', truncating`
1012
+ )();
1013
+ break;
1014
+ }
1015
+ const expectedW = Math.max(1, baseWidth >> i);
1016
+ const expectedH = Math.max(1, baseHeight >> i);
1017
+ if (level.width !== expectedW || level.height !== expectedH) {
1018
+ log.warn(
1019
+ `createCompressedTexture: mip level ${i} dimensions ${level.width}x${level.height} ` +
1020
+ `don't match expected ${expectedW}x${expectedH}, truncating`
1021
+ )();
1022
+ break;
1023
+ }
1024
+ validLevelCount++;
1025
+ }
1026
+
1027
+ const texture = device.createTexture({
1028
+ ...baseOptions,
1029
+ format,
1030
+ usage: Texture.TEXTURE | Texture.COPY_DST,
1031
+ width: baseWidth,
1032
+ height: baseHeight,
1033
+ mipLevels: validLevelCount,
1034
+ data: baseLevel.data
1035
+ });
1036
+
1037
+ // Upload additional validated mip levels
1038
+ for (let i = 1; i < validLevelCount; i++) {
1039
+ texture.writeData(levels[i].data, {
1040
+ width: levels[i].width,
1041
+ height: levels[i].height,
1042
+ mipLevel: i
1043
+ });
1044
+ }
1045
+
1046
+ return texture;
1047
+ }
1048
+
270
1049
  /*
271
1050
  /**
272
1051
  * Parses a GLTF material definition into uniforms and parameters for the PBR shader module
@@ -312,7 +1091,7 @@ export class PBRMaterialParser {
312
1091
  if (imageBasedLightingEnvironment) {
313
1092
  this.bindings.pbr_diffuseEnvSampler = imageBasedLightingEnvironment.getDiffuseEnvSampler();
314
1093
  this.bindings.pbr_specularEnvSampler = imageBasedLightingEnvironment.getSpecularEnvSampler();
315
- this.bindings.pbr_BrdfLUT = imageBasedLightingEnvironment.getBrdfTexture();
1094
+ this.bindings.pbr_brdfLUT = imageBasedLightingEnvironment.getBrdfTexture();
316
1095
  this.uniforms.scaleIBLAmbient = [1, 1];
317
1096
  }
318
1097
 
@@ -382,8 +1161,13 @@ export class PBRMaterialParser {
382
1161
  log.warn('BLEND alphaMode might not work well because it requires mesh sorting')();
383
1162
  Object.assign(this.parameters, {
384
1163
  blend: true,
385
- blendEquation: GL.FUNC_ADD,
386
- blendFunc: [GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA]
1164
+ blendEquation: GLEnum.FUNC_ADD,
1165
+ blendFunc: [
1166
+ GLEnum.SRC_ALPHA,
1167
+ GLEnum.ONE_MINUS_SRC_ALPHA,
1168
+ GLEnum.ONE,
1169
+ GLEnum.ONE_MINUS_SRC_ALPHA
1170
+ ]
387
1171
  });
388
1172
  }
389
1173
  }
@@ -420,7 +1204,8 @@ export class PBRMaterialParser {
420
1204
  if (image.compressed) {
421
1205
  textureOptions = image;
422
1206
  specialTextureParameters = {
423
- [GL.TEXTURE_MIN_FILTER]: image.data.length > 1 ? GL.LINEAR_MIPMAP_NEAREST : GL.LINEAR
1207
+ [GLEnum.TEXTURE_MIN_FILTER]:
1208
+ image.data.length > 1 ? GLEnum.LINEAR_MIPMAP_NEAREST : GLEnum.LINEAR
424
1209
  };
425
1210
  } else {
426
1211
  // Texture2D accepts a promise that returns an image as data (Async Textures)
@@ -434,7 +1219,7 @@ export class PBRMaterialParser {
434
1219
  ...specialTextureParameters
435
1220
  },
436
1221
  pixelStore: {
437
- [GL.UNPACK_FLIP_Y_WEBGL]: false
1222
+ [GLEnum.UNPACK_FLIP_Y_WEBGL]: false
438
1223
  },
439
1224
  ...textureOptions
440
1225
  });