@luma.gl/gltf 9.2.6 → 9.3.0-alpha.11

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