@luma.gl/gltf 9.2.6 → 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
@@ -1,8 +1,8 @@
1
1
  // luma.gl
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
- import { GL } from '@luma.gl/constants';
5
- import { log } from '@luma.gl/core';
4
+ import { Texture, log, textureFormatDecoder } from '@luma.gl/core';
5
+ import { GLEnum } from "../webgl-to-webgpu/gltf-webgl-constants.js";
6
6
  import { convertSampler } from "../webgl-to-webgpu/convert-webgl-sampler.js";
7
7
  /**
8
8
  * Parses a GLTF material definition into uniforms and parameters for the PBR shader module
@@ -32,7 +32,8 @@ export function parsePBRMaterial(device, material, attributes, options) {
32
32
  imageBasedLightingEnvironment.diffuseEnvSampler.texture;
33
33
  parsedMaterial.bindings.pbr_specularEnvSampler =
34
34
  imageBasedLightingEnvironment.specularEnvSampler.texture;
35
- parsedMaterial.bindings.pbr_BrdfLUT = imageBasedLightingEnvironment.brdfLutTexture.texture;
35
+ parsedMaterial.bindings.pbr_brdfLUT = imageBasedLightingEnvironment.brdfLutTexture.texture;
36
+ parsedMaterial.uniforms.IBLenabled = true;
36
37
  parsedMaterial.uniforms.scaleIBLAmbient = [1, 1];
37
38
  }
38
39
  if (options?.pbrDebug) {
@@ -47,102 +48,610 @@ export function parsePBRMaterial(device, material, attributes, options) {
47
48
  parsedMaterial.defines['HAS_TANGENTS'] = true;
48
49
  if (attributes['TEXCOORD_0'])
49
50
  parsedMaterial.defines['HAS_UV'] = true;
51
+ if (attributes['JOINTS_0'] && attributes['WEIGHTS_0'])
52
+ parsedMaterial.defines['HAS_SKIN'] = true;
53
+ if (attributes['COLOR_0'])
54
+ parsedMaterial.defines['HAS_COLORS'] = true;
50
55
  if (options?.imageBasedLightingEnvironment)
51
56
  parsedMaterial.defines['USE_IBL'] = true;
52
57
  if (options?.lights)
53
58
  parsedMaterial.defines['USE_LIGHTS'] = true;
54
59
  if (material) {
55
- parseMaterial(device, material, parsedMaterial);
60
+ if (options.validateAttributes !== false) {
61
+ warnOnMissingExpectedAttributes(material, attributes);
62
+ }
63
+ parseMaterial(device, material, parsedMaterial, options.gltf);
56
64
  }
57
65
  return parsedMaterial;
58
66
  }
67
+ function warnOnMissingExpectedAttributes(material, attributes) {
68
+ const uvDependentTextureSlots = getUvDependentTextureSlots(material);
69
+ if (uvDependentTextureSlots.length > 0 && !attributes['TEXCOORD_0']) {
70
+ log.warn(`glTF material uses ${uvDependentTextureSlots.join(', ')} but primitive is missing TEXCOORD_0; textured shading will sample the default UV coordinates`)();
71
+ }
72
+ const isUnlitMaterial = Boolean(material.unlit || material.extensions?.KHR_materials_unlit);
73
+ if (isUnlitMaterial || attributes['NORMAL']) {
74
+ return;
75
+ }
76
+ const missingNormalReason = material.normalTexture
77
+ ? 'lit PBR shading with normalTexture'
78
+ : 'lit PBR shading';
79
+ log.warn(`glTF primitive is missing NORMAL while using ${missingNormalReason}; shading will fall back to geometric normals`)();
80
+ }
81
+ function getUvDependentTextureSlots(material) {
82
+ const uvDependentTextureSlots = [];
83
+ if (material.pbrMetallicRoughness?.baseColorTexture) {
84
+ uvDependentTextureSlots.push('baseColorTexture');
85
+ }
86
+ if (material.pbrMetallicRoughness?.metallicRoughnessTexture) {
87
+ uvDependentTextureSlots.push('metallicRoughnessTexture');
88
+ }
89
+ if (material.normalTexture) {
90
+ uvDependentTextureSlots.push('normalTexture');
91
+ }
92
+ if (material.occlusionTexture) {
93
+ uvDependentTextureSlots.push('occlusionTexture');
94
+ }
95
+ if (material.emissiveTexture) {
96
+ uvDependentTextureSlots.push('emissiveTexture');
97
+ }
98
+ if (material.extensions?.KHR_materials_specular?.specularTexture) {
99
+ uvDependentTextureSlots.push('KHR_materials_specular.specularTexture');
100
+ }
101
+ if (material.extensions?.KHR_materials_specular?.specularColorTexture) {
102
+ uvDependentTextureSlots.push('KHR_materials_specular.specularColorTexture');
103
+ }
104
+ if (material.extensions?.KHR_materials_transmission?.transmissionTexture) {
105
+ uvDependentTextureSlots.push('KHR_materials_transmission.transmissionTexture');
106
+ }
107
+ if (material.extensions?.KHR_materials_clearcoat?.clearcoatTexture) {
108
+ uvDependentTextureSlots.push('KHR_materials_clearcoat.clearcoatTexture');
109
+ }
110
+ if (material.extensions?.KHR_materials_clearcoat?.clearcoatRoughnessTexture) {
111
+ uvDependentTextureSlots.push('KHR_materials_clearcoat.clearcoatRoughnessTexture');
112
+ }
113
+ if (material.extensions?.KHR_materials_sheen?.sheenColorTexture) {
114
+ uvDependentTextureSlots.push('KHR_materials_sheen.sheenColorTexture');
115
+ }
116
+ if (material.extensions?.KHR_materials_sheen?.sheenRoughnessTexture) {
117
+ uvDependentTextureSlots.push('KHR_materials_sheen.sheenRoughnessTexture');
118
+ }
119
+ if (material.extensions?.KHR_materials_iridescence?.iridescenceTexture) {
120
+ uvDependentTextureSlots.push('KHR_materials_iridescence.iridescenceTexture');
121
+ }
122
+ if (material.extensions?.KHR_materials_anisotropy?.anisotropyTexture) {
123
+ uvDependentTextureSlots.push('KHR_materials_anisotropy.anisotropyTexture');
124
+ }
125
+ return uvDependentTextureSlots;
126
+ }
59
127
  /** Parse GLTF material record */
60
- function parseMaterial(device, material, parsedMaterial) {
61
- parsedMaterial.uniforms.unlit = Boolean(material.unlit);
128
+ function parseMaterial(device, material, parsedMaterial, gltf) {
129
+ parsedMaterial.uniforms.unlit = Boolean(material.unlit || material.extensions?.KHR_materials_unlit);
62
130
  if (material.pbrMetallicRoughness) {
63
- parsePbrMetallicRoughness(device, material.pbrMetallicRoughness, parsedMaterial);
131
+ parsePbrMetallicRoughness(device, material.pbrMetallicRoughness, parsedMaterial, gltf);
64
132
  }
65
133
  if (material.normalTexture) {
66
- addTexture(device, material.normalTexture, 'pbr_normalSampler', 'HAS_NORMALMAP', parsedMaterial);
134
+ addTexture(device, material.normalTexture, 'pbr_normalSampler', parsedMaterial, {
135
+ featureOptions: {
136
+ define: 'HAS_NORMALMAP',
137
+ enabledUniformName: 'normalMapEnabled'
138
+ },
139
+ gltf
140
+ });
67
141
  const { scale = 1 } = material.normalTexture;
68
142
  parsedMaterial.uniforms.normalScale = scale;
69
143
  }
70
144
  if (material.occlusionTexture) {
71
- addTexture(device, material.occlusionTexture, 'pbr_occlusionSampler', 'HAS_OCCLUSIONMAP', parsedMaterial);
145
+ addTexture(device, material.occlusionTexture, 'pbr_occlusionSampler', parsedMaterial, {
146
+ featureOptions: {
147
+ define: 'HAS_OCCLUSIONMAP',
148
+ enabledUniformName: 'occlusionMapEnabled'
149
+ },
150
+ gltf
151
+ });
72
152
  const { strength = 1 } = material.occlusionTexture;
73
153
  parsedMaterial.uniforms.occlusionStrength = strength;
74
154
  }
155
+ parsedMaterial.uniforms.emissiveFactor = material.emissiveFactor || [0, 0, 0];
75
156
  if (material.emissiveTexture) {
76
- addTexture(device, material.emissiveTexture, 'pbr_emissiveSampler', 'HAS_EMISSIVEMAP', parsedMaterial);
77
- parsedMaterial.uniforms.emissiveFactor = material.emissiveFactor || [0, 0, 0];
157
+ addTexture(device, material.emissiveTexture, 'pbr_emissiveSampler', parsedMaterial, {
158
+ featureOptions: {
159
+ define: 'HAS_EMISSIVEMAP',
160
+ enabledUniformName: 'emissiveMapEnabled'
161
+ },
162
+ gltf
163
+ });
78
164
  }
79
- switch (material.alphaMode || 'MASK') {
80
- case 'MASK':
165
+ parseMaterialExtensions(device, material.extensions, parsedMaterial, gltf);
166
+ switch (material.alphaMode || 'OPAQUE') {
167
+ case 'OPAQUE':
168
+ break;
169
+ case 'MASK': {
81
170
  const { alphaCutoff = 0.5 } = material;
82
171
  parsedMaterial.defines['ALPHA_CUTOFF'] = true;
172
+ parsedMaterial.uniforms.alphaCutoffEnabled = true;
83
173
  parsedMaterial.uniforms.alphaCutoff = alphaCutoff;
84
174
  break;
175
+ }
85
176
  case 'BLEND':
86
177
  log.warn('glTF BLEND alphaMode might not work well because it requires mesh sorting')();
87
- // WebGPU style parameters
88
- parsedMaterial.parameters.blend = true;
89
- parsedMaterial.parameters.blendColorOperation = 'add';
90
- parsedMaterial.parameters.blendColorSrcFactor = 'src-alpha';
91
- parsedMaterial.parameters.blendColorDstFactor = 'one-minus-src-alpha';
92
- parsedMaterial.parameters.blendAlphaOperation = 'add';
93
- parsedMaterial.parameters.blendAlphaSrcFactor = 'one';
94
- parsedMaterial.parameters.blendAlphaDstFactor = 'one-minus-src-alpha';
95
- // GL parameters
96
- // TODO - remove in favor of parameters
97
- parsedMaterial.glParameters['blend'] = true;
98
- parsedMaterial.glParameters['blendEquation'] = 32774;
99
- parsedMaterial.glParameters['blendFunc'] = [
100
- 770,
101
- 771,
102
- 1,
103
- 771
104
- ];
178
+ applyAlphaBlendParameters(parsedMaterial);
105
179
  break;
106
180
  }
107
181
  }
182
+ function applyAlphaBlendParameters(parsedMaterial) {
183
+ parsedMaterial.parameters.blend = true;
184
+ parsedMaterial.parameters.blendColorOperation = 'add';
185
+ parsedMaterial.parameters.blendColorSrcFactor = 'src-alpha';
186
+ parsedMaterial.parameters.blendColorDstFactor = 'one-minus-src-alpha';
187
+ parsedMaterial.parameters.blendAlphaOperation = 'add';
188
+ parsedMaterial.parameters.blendAlphaSrcFactor = 'one';
189
+ parsedMaterial.parameters.blendAlphaDstFactor = 'one-minus-src-alpha';
190
+ parsedMaterial.glParameters['blend'] = true;
191
+ parsedMaterial.glParameters['blendEquation'] = GLEnum.FUNC_ADD;
192
+ parsedMaterial.glParameters['blendFunc'] = [
193
+ GLEnum.SRC_ALPHA,
194
+ GLEnum.ONE_MINUS_SRC_ALPHA,
195
+ GLEnum.ONE,
196
+ GLEnum.ONE_MINUS_SRC_ALPHA
197
+ ];
198
+ }
199
+ function applyTransmissionBlendApproximation(parsedMaterial) {
200
+ parsedMaterial.parameters.blend = true;
201
+ parsedMaterial.parameters.depthWriteEnabled = false;
202
+ parsedMaterial.parameters.blendColorOperation = 'add';
203
+ parsedMaterial.parameters.blendColorSrcFactor = 'one';
204
+ parsedMaterial.parameters.blendColorDstFactor = 'one-minus-src-alpha';
205
+ parsedMaterial.parameters.blendAlphaOperation = 'add';
206
+ parsedMaterial.parameters.blendAlphaSrcFactor = 'one';
207
+ parsedMaterial.parameters.blendAlphaDstFactor = 'one-minus-src-alpha';
208
+ parsedMaterial.glParameters['blend'] = true;
209
+ parsedMaterial.glParameters['depthMask'] = false;
210
+ parsedMaterial.glParameters['blendEquation'] = GLEnum.FUNC_ADD;
211
+ parsedMaterial.glParameters['blendFunc'] = [
212
+ GLEnum.ONE,
213
+ GLEnum.ONE_MINUS_SRC_ALPHA,
214
+ GLEnum.ONE,
215
+ GLEnum.ONE_MINUS_SRC_ALPHA
216
+ ];
217
+ }
108
218
  /** Parse GLTF material sub record */
109
- function parsePbrMetallicRoughness(device, pbrMetallicRoughness, parsedMaterial) {
219
+ function parsePbrMetallicRoughness(device, pbrMetallicRoughness, parsedMaterial, gltf) {
110
220
  if (pbrMetallicRoughness.baseColorTexture) {
111
- addTexture(device, pbrMetallicRoughness.baseColorTexture, 'pbr_baseColorSampler', 'HAS_BASECOLORMAP', parsedMaterial);
221
+ addTexture(device, pbrMetallicRoughness.baseColorTexture, 'pbr_baseColorSampler', parsedMaterial, {
222
+ featureOptions: {
223
+ define: 'HAS_BASECOLORMAP',
224
+ enabledUniformName: 'baseColorMapEnabled'
225
+ },
226
+ gltf
227
+ });
112
228
  }
113
229
  parsedMaterial.uniforms.baseColorFactor = pbrMetallicRoughness.baseColorFactor || [1, 1, 1, 1];
114
230
  if (pbrMetallicRoughness.metallicRoughnessTexture) {
115
- addTexture(device, pbrMetallicRoughness.metallicRoughnessTexture, 'pbr_metallicRoughnessSampler', 'HAS_METALROUGHNESSMAP', parsedMaterial);
231
+ addTexture(device, pbrMetallicRoughness.metallicRoughnessTexture, 'pbr_metallicRoughnessSampler', parsedMaterial, {
232
+ featureOptions: {
233
+ define: 'HAS_METALROUGHNESSMAP',
234
+ enabledUniformName: 'metallicRoughnessMapEnabled'
235
+ },
236
+ gltf
237
+ });
116
238
  }
117
239
  const { metallicFactor = 1, roughnessFactor = 1 } = pbrMetallicRoughness;
118
240
  parsedMaterial.uniforms.metallicRoughnessValues = [metallicFactor, roughnessFactor];
119
241
  }
120
- /** Create a texture from a glTF texture/sampler/image combo and add it to bindings */
121
- function addTexture(device, gltfTexture, uniformName, define, parsedMaterial) {
122
- const image = gltfTexture.texture.source.image;
123
- let textureOptions;
124
- if (image.compressed) {
125
- textureOptions = image;
242
+ function parseMaterialExtensions(device, extensions, parsedMaterial, gltf) {
243
+ if (!extensions) {
244
+ return;
126
245
  }
127
- else {
128
- // Texture2D accepts a promise that returns an image as data (Async Textures)
129
- textureOptions = { data: image };
246
+ if (hasMaterialExtensionShading(extensions)) {
247
+ parsedMaterial.defines['USE_MATERIAL_EXTENSIONS'] = true;
248
+ }
249
+ parseSpecularExtension(device, extensions.KHR_materials_specular, parsedMaterial, gltf);
250
+ parseIorExtension(extensions.KHR_materials_ior, parsedMaterial);
251
+ parseTransmissionExtension(device, extensions.KHR_materials_transmission, parsedMaterial, gltf);
252
+ parseVolumeExtension(device, extensions.KHR_materials_volume, parsedMaterial, gltf);
253
+ parseClearcoatExtension(device, extensions.KHR_materials_clearcoat, parsedMaterial, gltf);
254
+ parseSheenExtension(device, extensions.KHR_materials_sheen, parsedMaterial, gltf);
255
+ parseIridescenceExtension(device, extensions.KHR_materials_iridescence, parsedMaterial, gltf);
256
+ parseAnisotropyExtension(device, extensions.KHR_materials_anisotropy, parsedMaterial, gltf);
257
+ parseEmissiveStrengthExtension(extensions.KHR_materials_emissive_strength, parsedMaterial);
258
+ }
259
+ function hasMaterialExtensionShading(extensions) {
260
+ return Boolean(extensions.KHR_materials_specular ||
261
+ extensions.KHR_materials_ior ||
262
+ extensions.KHR_materials_transmission ||
263
+ extensions.KHR_materials_volume ||
264
+ extensions.KHR_materials_clearcoat ||
265
+ extensions.KHR_materials_sheen ||
266
+ extensions.KHR_materials_iridescence ||
267
+ extensions.KHR_materials_anisotropy);
268
+ }
269
+ function parseSpecularExtension(device, extension, parsedMaterial, gltf) {
270
+ if (!extension) {
271
+ return;
272
+ }
273
+ if (extension.specularColorFactor) {
274
+ parsedMaterial.uniforms.specularColorFactor = extension.specularColorFactor;
275
+ }
276
+ if (extension.specularFactor !== undefined) {
277
+ parsedMaterial.uniforms.specularIntensityFactor = extension.specularFactor;
278
+ }
279
+ if (extension.specularColorTexture) {
280
+ addTexture(device, extension.specularColorTexture, 'pbr_specularColorSampler', parsedMaterial, {
281
+ featureOptions: {
282
+ define: 'HAS_SPECULARCOLORMAP',
283
+ enabledUniformName: 'specularColorMapEnabled'
284
+ },
285
+ gltf
286
+ });
287
+ }
288
+ if (extension.specularTexture) {
289
+ addTexture(device, extension.specularTexture, 'pbr_specularIntensitySampler', parsedMaterial, {
290
+ featureOptions: {
291
+ define: 'HAS_SPECULARINTENSITYMAP',
292
+ enabledUniformName: 'specularIntensityMapEnabled'
293
+ },
294
+ gltf
295
+ });
296
+ }
297
+ }
298
+ function parseIorExtension(extension, parsedMaterial) {
299
+ if (extension?.ior !== undefined) {
300
+ parsedMaterial.uniforms.ior = extension.ior;
301
+ }
302
+ }
303
+ function parseTransmissionExtension(device, extension, parsedMaterial, gltf) {
304
+ if (!extension) {
305
+ return;
306
+ }
307
+ if (extension.transmissionFactor !== undefined) {
308
+ parsedMaterial.uniforms.transmissionFactor = extension.transmissionFactor;
309
+ }
310
+ if (extension.transmissionTexture) {
311
+ addTexture(device, extension.transmissionTexture, 'pbr_transmissionSampler', parsedMaterial, {
312
+ featureOptions: {
313
+ define: 'HAS_TRANSMISSIONMAP',
314
+ enabledUniformName: 'transmissionMapEnabled'
315
+ },
316
+ gltf
317
+ });
318
+ }
319
+ if ((extension.transmissionFactor ?? 0) > 0 || extension.transmissionTexture) {
320
+ log.warn('KHR_materials_transmission uses a premultiplied-alpha blending approximation and may require mesh sorting')();
321
+ applyTransmissionBlendApproximation(parsedMaterial);
322
+ }
323
+ }
324
+ function parseVolumeExtension(device, extension, parsedMaterial, gltf) {
325
+ if (!extension) {
326
+ return;
327
+ }
328
+ if (extension.thicknessFactor !== undefined) {
329
+ parsedMaterial.uniforms.thicknessFactor = extension.thicknessFactor;
330
+ }
331
+ if (extension.thicknessTexture) {
332
+ addTexture(device, extension.thicknessTexture, 'pbr_thicknessSampler', parsedMaterial, {
333
+ featureOptions: {
334
+ define: 'HAS_THICKNESSMAP'
335
+ },
336
+ gltf
337
+ });
338
+ }
339
+ if (extension.attenuationDistance !== undefined) {
340
+ parsedMaterial.uniforms.attenuationDistance = extension.attenuationDistance;
341
+ }
342
+ if (extension.attenuationColor) {
343
+ parsedMaterial.uniforms.attenuationColor = extension.attenuationColor;
344
+ }
345
+ }
346
+ function parseClearcoatExtension(device, extension, parsedMaterial, gltf) {
347
+ if (!extension) {
348
+ return;
349
+ }
350
+ if (extension.clearcoatFactor !== undefined) {
351
+ parsedMaterial.uniforms.clearcoatFactor = extension.clearcoatFactor;
352
+ }
353
+ if (extension.clearcoatRoughnessFactor !== undefined) {
354
+ parsedMaterial.uniforms.clearcoatRoughnessFactor = extension.clearcoatRoughnessFactor;
355
+ }
356
+ if (extension.clearcoatTexture) {
357
+ addTexture(device, extension.clearcoatTexture, 'pbr_clearcoatSampler', parsedMaterial, {
358
+ featureOptions: {
359
+ define: 'HAS_CLEARCOATMAP',
360
+ enabledUniformName: 'clearcoatMapEnabled'
361
+ },
362
+ gltf
363
+ });
364
+ }
365
+ if (extension.clearcoatRoughnessTexture) {
366
+ addTexture(device, extension.clearcoatRoughnessTexture, 'pbr_clearcoatRoughnessSampler', parsedMaterial, {
367
+ featureOptions: {
368
+ define: 'HAS_CLEARCOATROUGHNESSMAP',
369
+ enabledUniformName: 'clearcoatRoughnessMapEnabled'
370
+ },
371
+ gltf
372
+ });
373
+ }
374
+ if (extension.clearcoatNormalTexture) {
375
+ addTexture(device, extension.clearcoatNormalTexture, 'pbr_clearcoatNormalSampler', parsedMaterial, {
376
+ featureOptions: {
377
+ define: 'HAS_CLEARCOATNORMALMAP'
378
+ },
379
+ gltf
380
+ });
381
+ }
382
+ }
383
+ function parseSheenExtension(device, extension, parsedMaterial, gltf) {
384
+ if (!extension) {
385
+ return;
386
+ }
387
+ if (extension.sheenColorFactor) {
388
+ parsedMaterial.uniforms.sheenColorFactor = extension.sheenColorFactor;
389
+ }
390
+ if (extension.sheenRoughnessFactor !== undefined) {
391
+ parsedMaterial.uniforms.sheenRoughnessFactor = extension.sheenRoughnessFactor;
392
+ }
393
+ if (extension.sheenColorTexture) {
394
+ addTexture(device, extension.sheenColorTexture, 'pbr_sheenColorSampler', parsedMaterial, {
395
+ featureOptions: {
396
+ define: 'HAS_SHEENCOLORMAP',
397
+ enabledUniformName: 'sheenColorMapEnabled'
398
+ },
399
+ gltf
400
+ });
401
+ }
402
+ if (extension.sheenRoughnessTexture) {
403
+ addTexture(device, extension.sheenRoughnessTexture, 'pbr_sheenRoughnessSampler', parsedMaterial, {
404
+ featureOptions: {
405
+ define: 'HAS_SHEENROUGHNESSMAP',
406
+ enabledUniformName: 'sheenRoughnessMapEnabled'
407
+ },
408
+ gltf
409
+ });
410
+ }
411
+ }
412
+ function parseIridescenceExtension(device, extension, parsedMaterial, gltf) {
413
+ if (!extension) {
414
+ return;
415
+ }
416
+ if (extension.iridescenceFactor !== undefined) {
417
+ parsedMaterial.uniforms.iridescenceFactor = extension.iridescenceFactor;
418
+ }
419
+ if (extension.iridescenceIor !== undefined) {
420
+ parsedMaterial.uniforms.iridescenceIor = extension.iridescenceIor;
421
+ }
422
+ if (extension.iridescenceThicknessMinimum !== undefined ||
423
+ extension.iridescenceThicknessMaximum !== undefined) {
424
+ parsedMaterial.uniforms.iridescenceThicknessRange = [
425
+ extension.iridescenceThicknessMinimum ?? 100,
426
+ extension.iridescenceThicknessMaximum ?? 400
427
+ ];
428
+ }
429
+ if (extension.iridescenceTexture) {
430
+ addTexture(device, extension.iridescenceTexture, 'pbr_iridescenceSampler', parsedMaterial, {
431
+ featureOptions: {
432
+ define: 'HAS_IRIDESCENCEMAP',
433
+ enabledUniformName: 'iridescenceMapEnabled'
434
+ },
435
+ gltf
436
+ });
437
+ }
438
+ if (extension.iridescenceThicknessTexture) {
439
+ addTexture(device, extension.iridescenceThicknessTexture, 'pbr_iridescenceThicknessSampler', parsedMaterial, {
440
+ featureOptions: {
441
+ define: 'HAS_IRIDESCENCETHICKNESSMAP'
442
+ },
443
+ gltf
444
+ });
445
+ }
446
+ }
447
+ function parseAnisotropyExtension(device, extension, parsedMaterial, gltf) {
448
+ if (!extension) {
449
+ return;
450
+ }
451
+ if (extension.anisotropyStrength !== undefined) {
452
+ parsedMaterial.uniforms.anisotropyStrength = extension.anisotropyStrength;
453
+ }
454
+ if (extension.anisotropyRotation !== undefined) {
455
+ parsedMaterial.uniforms.anisotropyRotation = extension.anisotropyRotation;
456
+ }
457
+ if (extension.anisotropyTexture) {
458
+ addTexture(device, extension.anisotropyTexture, 'pbr_anisotropySampler', parsedMaterial, {
459
+ featureOptions: {
460
+ define: 'HAS_ANISOTROPYMAP',
461
+ enabledUniformName: 'anisotropyMapEnabled'
462
+ },
463
+ gltf
464
+ });
465
+ }
466
+ }
467
+ function parseEmissiveStrengthExtension(extension, parsedMaterial) {
468
+ if (extension?.emissiveStrength !== undefined) {
469
+ parsedMaterial.uniforms.emissiveStrength = extension.emissiveStrength;
470
+ }
471
+ }
472
+ /** Create a texture from a glTF texture/sampler/image combo and add it to bindings */
473
+ function addTexture(device, gltfTexture, uniformName, parsedMaterial, textureParseOptions = {}) {
474
+ const { featureOptions = {}, gltf } = textureParseOptions;
475
+ const { define, enabledUniformName } = featureOptions;
476
+ const resolvedTextureInfo = resolveTextureInfo(gltfTexture, gltf);
477
+ const image = resolvedTextureInfo.texture?.source?.image;
478
+ if (!image) {
479
+ log.warn(`Skipping unresolved glTF texture for ${String(uniformName)}`)();
480
+ return;
130
481
  }
131
482
  const gltfSampler = {
132
483
  wrapS: 10497, // default REPEAT S (U) wrapping mode.
133
484
  wrapT: 10497, // default REPEAT T (V) wrapping mode.
134
- ...gltfTexture?.texture?.sampler
485
+ minFilter: 9729, // default LINEAR filtering
486
+ magFilter: 9729, // default LINEAR filtering
487
+ ...resolvedTextureInfo?.texture?.sampler
135
488
  };
136
- const texture = device.createTexture({
137
- id: gltfTexture.uniformName || gltfTexture.id,
138
- sampler: convertSampler(gltfSampler),
139
- ...textureOptions
140
- });
489
+ const baseOptions = {
490
+ id: resolvedTextureInfo.uniformName || resolvedTextureInfo.id,
491
+ sampler: convertSampler(gltfSampler)
492
+ };
493
+ let texture;
494
+ if (image.compressed) {
495
+ texture = createCompressedTexture(device, image, baseOptions);
496
+ }
497
+ else {
498
+ const { width, height } = device.getExternalImageSize(image);
499
+ texture = device.createTexture({
500
+ ...baseOptions,
501
+ width,
502
+ height,
503
+ data: image
504
+ });
505
+ }
141
506
  parsedMaterial.bindings[uniformName] = texture;
142
507
  if (define)
143
508
  parsedMaterial.defines[define] = true;
509
+ if (enabledUniformName) {
510
+ parsedMaterial.uniforms[enabledUniformName] = true;
511
+ }
144
512
  parsedMaterial.generatedTextures.push(texture);
145
513
  }
514
+ function resolveTextureInfo(gltfTexture, gltf) {
515
+ if (gltfTexture.texture || gltfTexture.index === undefined || !gltf?.textures) {
516
+ return gltfTexture;
517
+ }
518
+ const resolvedTextureEntry = gltf.textures[gltfTexture.index];
519
+ if (!resolvedTextureEntry) {
520
+ return gltfTexture;
521
+ }
522
+ if ('texture' in resolvedTextureEntry && resolvedTextureEntry.texture) {
523
+ return {
524
+ ...resolvedTextureEntry,
525
+ ...gltfTexture,
526
+ texture: resolvedTextureEntry.texture
527
+ };
528
+ }
529
+ if (!('source' in resolvedTextureEntry)) {
530
+ return gltfTexture;
531
+ }
532
+ return {
533
+ ...gltfTexture,
534
+ texture: resolvedTextureEntry
535
+ };
536
+ }
537
+ function createCompressedTextureFallback(device, baseOptions) {
538
+ return device.createTexture({
539
+ ...baseOptions,
540
+ format: 'rgba8unorm',
541
+ width: 1,
542
+ height: 1,
543
+ mipLevels: 1
544
+ });
545
+ }
546
+ function resolveCompressedTextureFormat(level) {
547
+ return level.textureFormat;
548
+ }
549
+ /**
550
+ * Maximum mip levels that can be filled for a compressed texture.
551
+ * texStorage2D allocates level i at (baseW >> i) × (baseH >> i).
552
+ * Compressed formats can't upload data for levels smaller than one block,
553
+ * so we stop before either dimension drops below the block size.
554
+ */
555
+ function getMaxCompressedMipLevels(baseWidth, baseHeight, format) {
556
+ const { blockWidth = 1, blockHeight = 1 } = textureFormatDecoder.getInfo(format);
557
+ let count = 1;
558
+ for (let i = 1;; i++) {
559
+ const w = Math.max(1, baseWidth >> i);
560
+ const h = Math.max(1, baseHeight >> i);
561
+ if (w < blockWidth || h < blockHeight)
562
+ break;
563
+ count++;
564
+ }
565
+ return count;
566
+ }
567
+ /**
568
+ * Create a texture from compressed image data produced by loaders.gl.
569
+ * Handles current loaders.gl compressed image layouts:
570
+ *
571
+ * current: {compressed, mipmaps: true, data: [{data, width, height, textureFormat}, ...]}
572
+ * forward: {compressed, mipmaps: [{data, width, height, textureFormat}, ...]}
573
+ */
574
+ export function createCompressedTexture(device, image, baseOptions) {
575
+ // Normalize mip levels from all known loaders.gl formats
576
+ let levels;
577
+ if (Array.isArray(image.data) && image.data[0]?.data) {
578
+ // loaders.gl current format: image.data is Array of mip-level objects
579
+ levels = image.data;
580
+ }
581
+ else if ('mipmaps' in image && Array.isArray(image.mipmaps)) {
582
+ // Hypothetical future format: image.mipmaps is an Array
583
+ levels = image.mipmaps;
584
+ }
585
+ else {
586
+ levels = [];
587
+ }
588
+ if (levels.length === 0 || !levels[0]?.data) {
589
+ log.warn('createCompressedTexture: compressed image has no valid mip levels, creating fallback')();
590
+ return createCompressedTextureFallback(device, baseOptions);
591
+ }
592
+ const baseLevel = levels[0];
593
+ const baseWidth = baseLevel.width ?? image.width ?? 0;
594
+ const baseHeight = baseLevel.height ?? image.height ?? 0;
595
+ if (baseWidth <= 0 || baseHeight <= 0) {
596
+ log.warn('createCompressedTexture: base level has invalid dimensions, creating fallback')();
597
+ return createCompressedTextureFallback(device, baseOptions);
598
+ }
599
+ const format = resolveCompressedTextureFormat(baseLevel);
600
+ if (!format) {
601
+ log.warn('createCompressedTexture: compressed image has no textureFormat, creating fallback')();
602
+ return createCompressedTextureFallback(device, baseOptions);
603
+ }
604
+ // Validate mip levels: truncate chain at first invalid level.
605
+ // Levels must be contiguous, so we stop at the first level that has
606
+ // a format mismatch, missing data, non-positive dimensions, or
607
+ // dimensions that don't match what texStorage2D will allocate.
608
+ //
609
+ // For block-compressed formats (ASTC, BC, ETC2), texStorage2D allocates
610
+ // mip levels down to 1×1 texels, but compressed data can't be smaller
611
+ // than one block (e.g. 4×4 for ASTC-4x4). Cap the chain so we never
612
+ // try to upload data whose block-aligned size exceeds the allocated level.
613
+ const maxMipLevels = getMaxCompressedMipLevels(baseWidth, baseHeight, format);
614
+ const levelLimit = Math.min(levels.length, maxMipLevels);
615
+ let validLevelCount = 1;
616
+ for (let i = 1; i < levelLimit; i++) {
617
+ const level = levels[i];
618
+ if (!level.data || level.width <= 0 || level.height <= 0) {
619
+ log.warn(`createCompressedTexture: mip level ${i} has invalid data/dimensions, truncating`)();
620
+ break;
621
+ }
622
+ const levelFormat = resolveCompressedTextureFormat(level);
623
+ if (levelFormat && levelFormat !== format) {
624
+ log.warn(`createCompressedTexture: mip level ${i} format '${levelFormat}' differs from base '${format}', truncating`)();
625
+ break;
626
+ }
627
+ const expectedW = Math.max(1, baseWidth >> i);
628
+ const expectedH = Math.max(1, baseHeight >> i);
629
+ if (level.width !== expectedW || level.height !== expectedH) {
630
+ log.warn(`createCompressedTexture: mip level ${i} dimensions ${level.width}x${level.height} ` +
631
+ `don't match expected ${expectedW}x${expectedH}, truncating`)();
632
+ break;
633
+ }
634
+ validLevelCount++;
635
+ }
636
+ const texture = device.createTexture({
637
+ ...baseOptions,
638
+ format,
639
+ usage: Texture.TEXTURE | Texture.COPY_DST,
640
+ width: baseWidth,
641
+ height: baseHeight,
642
+ mipLevels: validLevelCount,
643
+ data: baseLevel.data
644
+ });
645
+ // Upload additional validated mip levels
646
+ for (let i = 1; i < validLevelCount; i++) {
647
+ texture.writeData(levels[i].data, {
648
+ width: levels[i].width,
649
+ height: levels[i].height,
650
+ mipLevel: i
651
+ });
652
+ }
653
+ return texture;
654
+ }
146
655
  /*
147
656
  /**
148
657
  * Parses a GLTF material definition into uniforms and parameters for the PBR shader module
@@ -188,7 +697,7 @@ export class PBRMaterialParser {
188
697
  if (imageBasedLightingEnvironment) {
189
698
  this.bindings.pbr_diffuseEnvSampler = imageBasedLightingEnvironment.getDiffuseEnvSampler();
190
699
  this.bindings.pbr_specularEnvSampler = imageBasedLightingEnvironment.getSpecularEnvSampler();
191
- this.bindings.pbr_BrdfLUT = imageBasedLightingEnvironment.getBrdfTexture();
700
+ this.bindings.pbr_brdfLUT = imageBasedLightingEnvironment.getBrdfTexture();
192
701
  this.uniforms.scaleIBLAmbient = [1, 1];
193
702
  }
194
703
 
@@ -202,6 +711,7 @@ export class PBRMaterialParser {
202
711
  this.defineIfPresent(attributes.NORMAL, 'HAS_NORMALS');
203
712
  this.defineIfPresent(attributes.TANGENT && useTangents, 'HAS_TANGENTS');
204
713
  this.defineIfPresent(attributes.TEXCOORD_0, 'HAS_UV');
714
+ this.defineIfPresent(attributes.COLOR_0, 'HAS_COLORS');
205
715
 
206
716
  this.defineIfPresent(imageBasedLightingEnvironment, 'USE_IBL');
207
717
  this.defineIfPresent(lights, 'USE_LIGHTS');
@@ -257,8 +767,13 @@ export class PBRMaterialParser {
257
767
  log.warn('BLEND alphaMode might not work well because it requires mesh sorting')();
258
768
  Object.assign(this.parameters, {
259
769
  blend: true,
260
- blendEquation: GL.FUNC_ADD,
261
- blendFunc: [GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA]
770
+ blendEquation: GLEnum.FUNC_ADD,
771
+ blendFunc: [
772
+ GLEnum.SRC_ALPHA,
773
+ GLEnum.ONE_MINUS_SRC_ALPHA,
774
+ GLEnum.ONE,
775
+ GLEnum.ONE_MINUS_SRC_ALPHA
776
+ ]
262
777
  });
263
778
  }
264
779
  }
@@ -295,7 +810,8 @@ export class PBRMaterialParser {
295
810
  if (image.compressed) {
296
811
  textureOptions = image;
297
812
  specialTextureParameters = {
298
- [GL.TEXTURE_MIN_FILTER]: image.data.length > 1 ? GL.LINEAR_MIPMAP_NEAREST : GL.LINEAR
813
+ [GLEnum.TEXTURE_MIN_FILTER]:
814
+ image.data.length > 1 ? GLEnum.LINEAR_MIPMAP_NEAREST : GLEnum.LINEAR
299
815
  };
300
816
  } else {
301
817
  // Texture2D accepts a promise that returns an image as data (Async Textures)
@@ -309,7 +825,7 @@ export class PBRMaterialParser {
309
825
  ...specialTextureParameters
310
826
  },
311
827
  pixelStore: {
312
- [GL.UNPACK_FLIP_Y_WEBGL]: false
828
+ [GLEnum.UNPACK_FLIP_Y_WEBGL]: false
313
829
  },
314
830
  ...textureOptions
315
831
  });