@luma.gl/gltf 9.2.5 → 9.3.0-alpha.10

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 (91) hide show
  1. package/dist/dist.dev.js +1362 -313
  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 +39 -2
  14. package/dist/gltf/create-scenegraph-from-gltf.d.ts.map +1 -1
  15. package/dist/gltf/create-scenegraph-from-gltf.js +76 -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 +1302 -276
  26. package/dist/index.cjs.map +4 -4
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -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 +5 -0
  36. package/dist/parsers/parse-gltf-lights.d.ts.map +1 -0
  37. package/dist/parsers/parse-gltf-lights.js +163 -0
  38. package/dist/parsers/parse-gltf-lights.js.map +1 -0
  39. package/dist/parsers/parse-gltf.d.ts +19 -2
  40. package/dist/parsers/parse-gltf.d.ts.map +1 -1
  41. package/dist/parsers/parse-gltf.js +101 -61
  42. package/dist/parsers/parse-gltf.js.map +1 -1
  43. package/dist/parsers/parse-pbr-material.d.ts +115 -2
  44. package/dist/parsers/parse-pbr-material.d.ts.map +1 -1
  45. package/dist/parsers/parse-pbr-material.js +570 -54
  46. package/dist/parsers/parse-pbr-material.js.map +1 -1
  47. package/dist/pbr/pbr-environment.d.ts +10 -4
  48. package/dist/pbr/pbr-environment.d.ts.map +1 -1
  49. package/dist/pbr/pbr-environment.js +18 -15
  50. package/dist/pbr/pbr-environment.js.map +1 -1
  51. package/dist/pbr/pbr-material.d.ts +13 -3
  52. package/dist/pbr/pbr-material.d.ts.map +1 -1
  53. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts +12 -1
  54. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts.map +1 -1
  55. package/dist/webgl-to-webgpu/convert-webgl-attribute.js +3 -0
  56. package/dist/webgl-to-webgpu/convert-webgl-attribute.js.map +1 -1
  57. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts +11 -5
  58. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts.map +1 -1
  59. package/dist/webgl-to-webgpu/convert-webgl-sampler.js +16 -12
  60. package/dist/webgl-to-webgpu/convert-webgl-sampler.js.map +1 -1
  61. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts +2 -9
  62. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts.map +1 -1
  63. package/dist/webgl-to-webgpu/convert-webgl-topology.js +2 -14
  64. package/dist/webgl-to-webgpu/convert-webgl-topology.js.map +1 -1
  65. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts +27 -0
  66. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts.map +1 -0
  67. package/dist/webgl-to-webgpu/gltf-webgl-constants.js +34 -0
  68. package/dist/webgl-to-webgpu/gltf-webgl-constants.js.map +1 -0
  69. package/package.json +8 -9
  70. package/src/gltf/animations/animations.ts +17 -5
  71. package/src/gltf/animations/interpolate.ts +49 -68
  72. package/src/gltf/create-gltf-model.ts +214 -48
  73. package/src/gltf/create-scenegraph-from-gltf.ts +134 -11
  74. package/src/gltf/gltf-animator.ts +34 -25
  75. package/src/gltf/gltf-extension-support.ts +214 -0
  76. package/src/index.ts +11 -2
  77. package/src/parsers/parse-gltf-animations.ts +94 -33
  78. package/src/parsers/parse-gltf-lights.ts +218 -0
  79. package/src/parsers/parse-gltf.ts +170 -90
  80. package/src/parsers/parse-pbr-material.ts +870 -80
  81. package/src/pbr/pbr-environment.ts +44 -21
  82. package/src/pbr/pbr-material.ts +18 -3
  83. package/src/webgl-to-webgpu/convert-webgl-attribute.ts +12 -1
  84. package/src/webgl-to-webgpu/convert-webgl-sampler.ts +38 -29
  85. package/src/webgl-to-webgpu/convert-webgl-topology.ts +2 -14
  86. package/src/webgl-to-webgpu/gltf-webgl-constants.ts +35 -0
  87. package/dist/utils/deep-copy.d.ts +0 -3
  88. package/dist/utils/deep-copy.d.ts.map +0 -1
  89. package/dist/utils/deep-copy.js +0 -21
  90. package/dist/utils/deep-copy.js.map +0 -1
  91. package/src/utils/deep-copy.ts +0 -22
@@ -2,13 +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 {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';
7
8
 
8
- import {log} from '@luma.gl/core';
9
9
  import {type ParsedPBRMaterial} from '../pbr/pbr-material';
10
10
  import {type PBREnvironment} from '../pbr/pbr-environment';
11
11
  import {type PBRMaterialBindings} from '@luma.gl/shadertools';
12
+ import {GLEnum} from '../webgl-to-webgpu/gltf-webgl-constants';
12
13
  import {convertSampler} from '../webgl-to-webgpu/convert-webgl-sampler';
13
14
 
14
15
  // TODO - synchronize the GLTF... types with loaders.gl
@@ -18,6 +19,7 @@ import {convertSampler} from '../webgl-to-webgpu/convert-webgl-sampler';
18
19
 
19
20
  type GLTFTexture = {
20
21
  id: string;
22
+ index?: number;
21
23
  texture: {source: {image: any}; sampler: {parameters: any}};
22
24
  uniformName?: string;
23
25
  // is this on all textures?
@@ -35,16 +37,114 @@ type GLTFPBRMetallicRoughness = {
35
37
  };
36
38
 
37
39
  type GLTFPBRMaterial = {
40
+ extensions?: GLTFMaterialExtensions;
38
41
  unlit?: boolean;
39
42
  pbrMetallicRoughness?: GLTFPBRMetallicRoughness;
40
43
  normalTexture?: GLTFTexture;
41
44
  occlusionTexture?: GLTFTexture;
42
45
  emissiveTexture?: GLTFTexture;
43
46
  emissiveFactor?: [number, number, number];
44
- alphaMode?: 'MASK' | 'BLEND';
47
+ alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
48
+ doubleSided?: boolean;
45
49
  alphaCutoff?: number;
46
50
  };
47
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
+
48
148
  export type ParsePBRMaterialOptions = {
49
149
  /** Debug PBR shader */
50
150
  pbrDebug?: boolean;
@@ -54,6 +154,10 @@ export type ParsePBRMaterialOptions = {
54
154
  useTangents?: boolean;
55
155
  /** provide an image based (texture cube) lighting environment */
56
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;
57
161
  };
58
162
 
59
163
  /**
@@ -92,7 +196,8 @@ export function parsePBRMaterial(
92
196
  imageBasedLightingEnvironment.diffuseEnvSampler.texture;
93
197
  parsedMaterial.bindings.pbr_specularEnvSampler =
94
198
  imageBasedLightingEnvironment.specularEnvSampler.texture;
95
- parsedMaterial.bindings.pbr_BrdfLUT = imageBasedLightingEnvironment.brdfLutTexture.texture;
199
+ parsedMaterial.bindings.pbr_brdfLUT = imageBasedLightingEnvironment.brdfLutTexture.texture;
200
+ parsedMaterial.uniforms.IBLenabled = true;
96
201
  parsedMaterial.uniforms.scaleIBLAmbient = [1, 1];
97
202
  }
98
203
 
@@ -106,111 +211,224 @@ export function parsePBRMaterial(
106
211
  if (attributes['NORMAL']) parsedMaterial.defines['HAS_NORMALS'] = true;
107
212
  if (attributes['TANGENT'] && options?.useTangents) parsedMaterial.defines['HAS_TANGENTS'] = true;
108
213
  if (attributes['TEXCOORD_0']) parsedMaterial.defines['HAS_UV'] = true;
214
+ if (attributes['JOINTS_0'] && attributes['WEIGHTS_0']) parsedMaterial.defines['HAS_SKIN'] = true;
215
+ if (attributes['COLOR_0']) parsedMaterial.defines['HAS_COLORS'] = true;
109
216
 
110
217
  if (options?.imageBasedLightingEnvironment) parsedMaterial.defines['USE_IBL'] = true;
111
218
  if (options?.lights) parsedMaterial.defines['USE_LIGHTS'] = true;
112
219
 
113
220
  if (material) {
114
- parseMaterial(device, material, parsedMaterial);
221
+ if (options.validateAttributes !== false) {
222
+ warnOnMissingExpectedAttributes(material, attributes);
223
+ }
224
+ parseMaterial(device, material, parsedMaterial, options.gltf);
115
225
  }
116
226
 
117
227
  return parsedMaterial;
118
228
  }
119
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
+
120
303
  /** Parse GLTF material record */
121
304
  function parseMaterial(
122
305
  device: Device,
123
306
  material: GLTFPBRMaterial,
124
- parsedMaterial: ParsedPBRMaterial
307
+ parsedMaterial: ParsedPBRMaterial,
308
+ gltf?: GLTFPostprocessed
125
309
  ): void {
126
- parsedMaterial.uniforms.unlit = Boolean(material.unlit);
310
+ parsedMaterial.uniforms.unlit = Boolean(
311
+ material.unlit || material.extensions?.KHR_materials_unlit
312
+ );
127
313
 
128
314
  if (material.pbrMetallicRoughness) {
129
- parsePbrMetallicRoughness(device, material.pbrMetallicRoughness, parsedMaterial);
315
+ parsePbrMetallicRoughness(device, material.pbrMetallicRoughness, parsedMaterial, gltf);
130
316
  }
131
317
  if (material.normalTexture) {
132
- addTexture(
133
- device,
134
- material.normalTexture,
135
- 'pbr_normalSampler',
136
- 'HAS_NORMALMAP',
137
- parsedMaterial
138
- );
318
+ addTexture(device, material.normalTexture, 'pbr_normalSampler', parsedMaterial, {
319
+ featureOptions: {
320
+ define: 'HAS_NORMALMAP',
321
+ enabledUniformName: 'normalMapEnabled'
322
+ },
323
+ gltf
324
+ });
139
325
 
140
326
  const {scale = 1} = material.normalTexture;
141
327
  parsedMaterial.uniforms.normalScale = scale;
142
328
  }
143
329
  if (material.occlusionTexture) {
144
- addTexture(
145
- device,
146
- material.occlusionTexture,
147
- 'pbr_occlusionSampler',
148
- 'HAS_OCCLUSIONMAP',
149
- parsedMaterial
150
- );
330
+ addTexture(device, material.occlusionTexture, 'pbr_occlusionSampler', parsedMaterial, {
331
+ featureOptions: {
332
+ define: 'HAS_OCCLUSIONMAP',
333
+ enabledUniformName: 'occlusionMapEnabled'
334
+ },
335
+ gltf
336
+ });
151
337
 
152
338
  const {strength = 1} = material.occlusionTexture;
153
339
  parsedMaterial.uniforms.occlusionStrength = strength;
154
340
  }
341
+ parsedMaterial.uniforms.emissiveFactor = material.emissiveFactor || [0, 0, 0];
155
342
  if (material.emissiveTexture) {
156
- addTexture(
157
- device,
158
- material.emissiveTexture,
159
- 'pbr_emissiveSampler',
160
- 'HAS_EMISSIVEMAP',
161
- parsedMaterial
162
- );
163
- 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
+ });
164
350
  }
165
351
 
166
- switch (material.alphaMode || 'MASK') {
167
- case 'MASK':
352
+ parseMaterialExtensions(device, material.extensions, parsedMaterial, gltf);
353
+
354
+ switch (material.alphaMode || 'OPAQUE') {
355
+ case 'OPAQUE':
356
+ break;
357
+ case 'MASK': {
168
358
  const {alphaCutoff = 0.5} = material;
169
359
  parsedMaterial.defines['ALPHA_CUTOFF'] = true;
360
+ parsedMaterial.uniforms.alphaCutoffEnabled = true;
170
361
  parsedMaterial.uniforms.alphaCutoff = alphaCutoff;
171
362
  break;
363
+ }
172
364
  case 'BLEND':
173
365
  log.warn('glTF BLEND alphaMode might not work well because it requires mesh sorting')();
366
+ applyAlphaBlendParameters(parsedMaterial);
174
367
 
175
- // WebGPU style parameters
176
- parsedMaterial.parameters.blend = true;
368
+ break;
369
+ }
370
+ }
177
371
 
178
- parsedMaterial.parameters.blendColorOperation = 'add';
179
- parsedMaterial.parameters.blendColorSrcFactor = 'src-alpha';
180
- 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';
181
380
 
182
- parsedMaterial.parameters.blendAlphaOperation = 'add';
183
- parsedMaterial.parameters.blendAlphaSrcFactor = 'one';
184
- 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
+ }
185
390
 
186
- // GL parameters
187
- // TODO - remove in favor of parameters
188
- parsedMaterial.glParameters['blend'] = true;
189
- parsedMaterial.glParameters['blendEquation'] = GL.FUNC_ADD;
190
- parsedMaterial.glParameters['blendFunc'] = [
191
- GL.SRC_ALPHA,
192
- GL.ONE_MINUS_SRC_ALPHA,
193
- GL.ONE,
194
- GL.ONE_MINUS_SRC_ALPHA
195
- ];
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';
196
400
 
197
- break;
198
- }
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
+ ];
199
410
  }
200
411
 
201
412
  /** Parse GLTF material sub record */
202
413
  function parsePbrMetallicRoughness(
203
414
  device: Device,
204
415
  pbrMetallicRoughness: GLTFPBRMetallicRoughness,
205
- parsedMaterial: ParsedPBRMaterial
416
+ parsedMaterial: ParsedPBRMaterial,
417
+ gltf?: GLTFPostprocessed
206
418
  ): void {
207
419
  if (pbrMetallicRoughness.baseColorTexture) {
208
420
  addTexture(
209
421
  device,
210
422
  pbrMetallicRoughness.baseColorTexture,
211
423
  'pbr_baseColorSampler',
212
- 'HAS_BASECOLORMAP',
213
- parsedMaterial
424
+ parsedMaterial,
425
+ {
426
+ featureOptions: {
427
+ define: 'HAS_BASECOLORMAP',
428
+ enabledUniformName: 'baseColorMapEnabled'
429
+ },
430
+ gltf
431
+ }
214
432
  );
215
433
  }
216
434
  parsedMaterial.uniforms.baseColorFactor = pbrMetallicRoughness.baseColorFactor || [1, 1, 1, 1];
@@ -220,49 +438,614 @@ function parsePbrMetallicRoughness(
220
438
  device,
221
439
  pbrMetallicRoughness.metallicRoughnessTexture,
222
440
  'pbr_metallicRoughnessSampler',
223
- 'HAS_METALROUGHNESSMAP',
224
- parsedMaterial
441
+ parsedMaterial,
442
+ {
443
+ featureOptions: {
444
+ define: 'HAS_METALROUGHNESSMAP',
445
+ enabledUniformName: 'metallicRoughnessMapEnabled'
446
+ },
447
+ gltf
448
+ }
225
449
  );
226
450
  }
227
451
  const {metallicFactor = 1, roughnessFactor = 1} = pbrMetallicRoughness;
228
452
  parsedMaterial.uniforms.metallicRoughnessValues = [metallicFactor, roughnessFactor];
229
453
  }
230
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
+
231
782
  /** Create a texture from a glTF texture/sampler/image combo and add it to bindings */
232
783
  function addTexture(
233
784
  device: Device,
234
785
  gltfTexture: GLTFTexture,
235
786
  uniformName: keyof PBRMaterialBindings,
236
- define: string,
237
- parsedMaterial: ParsedPBRMaterial
787
+ parsedMaterial: ParsedPBRMaterial,
788
+ textureParseOptions: TextureParseOptions = {}
238
789
  ): void {
239
- const image = gltfTexture.texture.source.image;
240
- let textureOptions;
241
-
242
- if (image.compressed) {
243
- textureOptions = image;
244
- } else {
245
- // Texture2D accepts a promise that returns an image as data (Async Textures)
246
- 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;
247
797
  }
248
798
 
249
799
  const gltfSampler = {
250
800
  wrapS: 10497, // default REPEAT S (U) wrapping mode.
251
801
  wrapT: 10497, // default REPEAT T (V) wrapping mode.
252
- ...gltfTexture?.texture?.sampler
253
- } as any;
802
+ minFilter: 9729, // default LINEAR filtering
803
+ magFilter: 9729, // default LINEAR filtering
804
+ ...resolvedTextureInfo?.texture?.sampler
805
+ } as GLTFSampler;
254
806
 
255
- const texture: Texture = device.createTexture({
256
- id: gltfTexture.uniformName || gltfTexture.id,
257
- sampler: convertSampler(gltfSampler),
258
- ...textureOptions
259
- });
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
+ }
260
825
 
261
826
  parsedMaterial.bindings[uniformName] = texture;
262
827
  if (define) parsedMaterial.defines[define] = true;
828
+ if (enabledUniformName) {
829
+ parsedMaterial.uniforms[enabledUniformName] = true;
830
+ }
263
831
  parsedMaterial.generatedTextures.push(texture);
264
832
  }
265
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
+
266
1049
  /*
267
1050
  /**
268
1051
  * Parses a GLTF material definition into uniforms and parameters for the PBR shader module
@@ -308,7 +1091,7 @@ export class PBRMaterialParser {
308
1091
  if (imageBasedLightingEnvironment) {
309
1092
  this.bindings.pbr_diffuseEnvSampler = imageBasedLightingEnvironment.getDiffuseEnvSampler();
310
1093
  this.bindings.pbr_specularEnvSampler = imageBasedLightingEnvironment.getSpecularEnvSampler();
311
- this.bindings.pbr_BrdfLUT = imageBasedLightingEnvironment.getBrdfTexture();
1094
+ this.bindings.pbr_brdfLUT = imageBasedLightingEnvironment.getBrdfTexture();
312
1095
  this.uniforms.scaleIBLAmbient = [1, 1];
313
1096
  }
314
1097
 
@@ -322,6 +1105,7 @@ export class PBRMaterialParser {
322
1105
  this.defineIfPresent(attributes.NORMAL, 'HAS_NORMALS');
323
1106
  this.defineIfPresent(attributes.TANGENT && useTangents, 'HAS_TANGENTS');
324
1107
  this.defineIfPresent(attributes.TEXCOORD_0, 'HAS_UV');
1108
+ this.defineIfPresent(attributes.COLOR_0, 'HAS_COLORS');
325
1109
 
326
1110
  this.defineIfPresent(imageBasedLightingEnvironment, 'USE_IBL');
327
1111
  this.defineIfPresent(lights, 'USE_LIGHTS');
@@ -377,8 +1161,13 @@ export class PBRMaterialParser {
377
1161
  log.warn('BLEND alphaMode might not work well because it requires mesh sorting')();
378
1162
  Object.assign(this.parameters, {
379
1163
  blend: true,
380
- blendEquation: GL.FUNC_ADD,
381
- 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
+ ]
382
1171
  });
383
1172
  }
384
1173
  }
@@ -415,7 +1204,8 @@ export class PBRMaterialParser {
415
1204
  if (image.compressed) {
416
1205
  textureOptions = image;
417
1206
  specialTextureParameters = {
418
- [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
419
1209
  };
420
1210
  } else {
421
1211
  // Texture2D accepts a promise that returns an image as data (Async Textures)
@@ -429,7 +1219,7 @@ export class PBRMaterialParser {
429
1219
  ...specialTextureParameters
430
1220
  },
431
1221
  pixelStore: {
432
- [GL.UNPACK_FLIP_Y_WEBGL]: false
1222
+ [GLEnum.UNPACK_FLIP_Y_WEBGL]: false
433
1223
  },
434
1224
  ...textureOptions
435
1225
  });