@galfus/gltf-loader 0.0.0-stage → 0.31.0-alpha

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.
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Shared GPU resource extraction and uploading for glTF geometries, PBR materials, and textures.
3
+ */
4
+
5
+ import type { Realm3D } from '@galfus/engine';
6
+ import type { ResolvedDecoders } from './decoders/resolver';
7
+ import { decodeDataUri, readAccessor } from './parser';
8
+ import type { GlTF, GltfMaterialAlphaModeResolver, GlTFPrimitive } from './types';
9
+
10
+ export interface UploadedGltfResources {
11
+ geometryMap: Map<string, number>; // key: `${meshIndex}_${primitiveIndex}` -> geometryId
12
+ materialMap: Map<number, number>; // gltfMaterialIndex -> realmMaterialId
13
+ textureMap: Map<number, number>; // gltfTextureIndex -> realmTextureId
14
+ createdGeometryIds: number[];
15
+ createdMaterialIds: number[];
16
+ createdTextureIds: number[];
17
+ }
18
+
19
+ /**
20
+ * Extracts raw image bytes from a glTF image descriptor.
21
+ */
22
+ async function getImageBytes(
23
+ gltf: GlTF,
24
+ buffers: Uint8Array[],
25
+ imageIndex: number,
26
+ externalResolver?: (uri: string) => Promise<Uint8Array>
27
+ ): Promise<Uint8Array | undefined> {
28
+ const image = gltf.images?.[imageIndex];
29
+ if (!image) return undefined;
30
+
31
+ if (image.bufferView !== undefined) {
32
+ const bv = gltf.bufferViews?.[image.bufferView];
33
+ if (bv) {
34
+ const buffer = buffers[bv.buffer];
35
+ if (buffer) {
36
+ const start = bv.byteOffset ?? 0;
37
+ return buffer.subarray(start, start + bv.byteLength);
38
+ }
39
+ }
40
+ }
41
+
42
+ if (image.uri && image.uri.startsWith('data:')) {
43
+ return decodeDataUri(image.uri);
44
+ }
45
+
46
+ if (image.uri && externalResolver) {
47
+ return await externalResolver(image.uri);
48
+ }
49
+
50
+ if (image.uri) throw new Error(`Cannot resolve external glTF image: ${image.uri}`);
51
+
52
+ return undefined;
53
+ }
54
+
55
+ /**
56
+ * Maps glTF 2.0 primitive rendering mode to the Galfus Engine PrimitiveTopology3D enum.
57
+ *
58
+ * glTF specification:
59
+ * - 0: POINTS -> 0 (Points)
60
+ * - 1: LINES -> 1 (Lines)
61
+ * - 2: LINE_LOOP -> 2 (LineStrip)
62
+ * - 3: LINE_STRIP -> 2 (LineStrip)
63
+ * - 4: TRIANGLES -> 3 (Triangles / TriangleList)
64
+ * - 5: TRIANGLE_STRIP -> 4 (TriangleStrip)
65
+ */
66
+ function mapGltfPrimitiveTopology(mode?: number): number {
67
+ switch (mode) {
68
+ case 0:
69
+ return 0; // Points
70
+ case 1:
71
+ return 1; // Lines
72
+ case 2:
73
+ case 3:
74
+ return 2; // LineStrip
75
+ case 5:
76
+ return 4; // TriangleStrip
77
+ case 4:
78
+ default:
79
+ return 3; // Triangles
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Uploads all shared geometries, textures, and PBR materials to the GPU for a glTF asset.
85
+ *
86
+ * @param realm The destination Realm3D.
87
+ * @param gltf The root glTF descriptor.
88
+ * @param buffers Array of resolved binary buffers.
89
+ * @param decoders Object holding optional active decoders (Draco, Meshopt).
90
+ * @param externalResolver Optional resolver callback for loading relative external assets.
91
+ * @returns Map of created GPU resource handles and ID registries.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const resources = await uploadGltfResources(realm, gltf, buffers, decoders);
96
+ * console.log(`Uploaded ${resources.createdGeometryIds.length} geometries to VRAM`);
97
+ * ```
98
+ */
99
+ export async function uploadGltfResources(
100
+ realm: Realm3D,
101
+ gltf: GlTF,
102
+ buffers: Uint8Array[],
103
+ decoders: ResolvedDecoders,
104
+ externalResolver?: (uri: string) => Promise<Uint8Array>,
105
+ resolveMaterialAlphaMode?: GltfMaterialAlphaModeResolver
106
+ ): Promise<UploadedGltfResources> {
107
+ const textureMap = new Map<number, number>();
108
+ const materialMap = new Map<number, number>();
109
+ const geometryMap = new Map<string, number>();
110
+ const textureBytes = new Map<number, Uint8Array>();
111
+
112
+ const createdTextureIds: number[] = [];
113
+ const createdMaterialIds: number[] = [];
114
+ const createdGeometryIds: number[] = [];
115
+
116
+ // Determine which textures are used as sRGB color maps (baseColor / emissive / diffuse)
117
+ const srgbTextureIndices = new Set<number>();
118
+ if (gltf.materials) {
119
+ for (const mat of gltf.materials) {
120
+ if (mat.pbrMetallicRoughness?.baseColorTexture) {
121
+ srgbTextureIndices.add(mat.pbrMetallicRoughness.baseColorTexture.index);
122
+ }
123
+ const specGloss = mat.extensions?.KHR_materials_pbrSpecularGlossiness;
124
+ if (specGloss?.diffuseTexture) {
125
+ srgbTextureIndices.add(specGloss.diffuseTexture.index);
126
+ }
127
+ if (mat.emissiveTexture) {
128
+ srgbTextureIndices.add(mat.emissiveTexture.index);
129
+ }
130
+ }
131
+ }
132
+
133
+ // 1. Upload Textures
134
+ if (gltf.textures) {
135
+ for (let tIdx = 0; tIdx < gltf.textures.length; tIdx++) {
136
+ const tex = gltf.textures[tIdx]!;
137
+ const sourceIdx = tex.source ?? tex.extensions?.KHR_texture_basisu?.source;
138
+ if (sourceIdx !== undefined) {
139
+ const imageBytes = await getImageBytes(gltf, buffers, sourceIdx, externalResolver);
140
+ if (imageBytes && imageBytes.byteLength > 0) {
141
+ if (resolveMaterialAlphaMode) textureBytes.set(tIdx, imageBytes);
142
+ const isSrgb = srgbTextureIndices.has(tIdx);
143
+ const textureId = realm.resources.loadTexture(imageBytes, {
144
+ srgb: isSrgb,
145
+ label: tex.name ?? `glTF_Texture_${tIdx}`
146
+ });
147
+ textureMap.set(tIdx, textureId);
148
+ createdTextureIds.push(textureId);
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // 2. Upload Materials
155
+ if (gltf.materials) {
156
+ for (let mIdx = 0; mIdx < gltf.materials.length; mIdx++) {
157
+ const mat = gltf.materials[mIdx]!;
158
+ const pbr = mat.pbrMetallicRoughness;
159
+ const specGloss = mat.extensions?.KHR_materials_pbrSpecularGlossiness;
160
+
161
+ const linearColor = pbr?.baseColorFactor ?? specGloss?.diffuseFactor ?? [1, 1, 1, 1];
162
+ const linearToSrgb = (value: number): number =>
163
+ value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;
164
+ const baseColor: [number, number, number, number] = [
165
+ linearToSrgb(linearColor[0]),
166
+ linearToSrgb(linearColor[1]),
167
+ linearToSrgb(linearColor[2]),
168
+ linearColor[3]
169
+ ];
170
+ const metallic = pbr?.metallicFactor ?? (specGloss ? 0 : 1);
171
+ const roughness =
172
+ pbr?.roughnessFactor ?? (specGloss ? 1.0 - (specGloss.glossinessFactor ?? 1.0) : 1.0);
173
+ const baseColorTextureId =
174
+ pbr?.baseColorTexture !== undefined
175
+ ? textureMap.get(pbr.baseColorTexture.index)
176
+ : specGloss?.diffuseTexture !== undefined
177
+ ? textureMap.get(specGloss.diffuseTexture.index)
178
+ : undefined;
179
+ const normalTextureId =
180
+ mat.normalTexture !== undefined ? textureMap.get(mat.normalTexture.index) : undefined;
181
+ const metallicRoughnessTextureId =
182
+ pbr?.metallicRoughnessTexture !== undefined
183
+ ? textureMap.get(pbr.metallicRoughnessTexture.index)
184
+ : undefined;
185
+ const emissiveTextureId =
186
+ mat.emissiveTexture !== undefined ? textureMap.get(mat.emissiveTexture.index) : undefined;
187
+ const occlusionTextureId =
188
+ mat.occlusionTexture !== undefined ? textureMap.get(mat.occlusionTexture.index) : undefined;
189
+
190
+ // The Galfus 3D shader computes emissive as (material.emissive_color.rgb + tex_emissive.rgb).
191
+ // When an emissive texture is present, avoid adding constant white factor which would blow out the surface.
192
+ const emissiveColor: [number, number, number, number] =
193
+ mat.emissiveFactor && emissiveTextureId === undefined
194
+ ? [mat.emissiveFactor[0], mat.emissiveFactor[1], mat.emissiveFactor[2], 1]
195
+ : [0, 0, 0, 1];
196
+
197
+ const baseTextureIndex = pbr?.baseColorTexture?.index ?? specGloss?.diffuseTexture?.index;
198
+ const alphaMode =
199
+ resolveMaterialAlphaMode?.(
200
+ mat,
201
+ mIdx,
202
+ baseTextureIndex === undefined ? undefined : textureBytes.get(baseTextureIndex)
203
+ ) ?? mat.alphaMode;
204
+ const surfaceType =
205
+ alphaMode === 'BLEND' ? 'transparent' : alphaMode === 'MASK' ? 'masked' : 'opaque';
206
+
207
+ const alphaCutoff = mat.alphaCutoff;
208
+ const ior = mat.extensions?.KHR_materials_ior?.ior ?? 1.5;
209
+ const reflectance = ior === 0 ? 2.5 : (2.5 * (ior - 1)) / (ior + 1);
210
+
211
+ const { materialId } = realm.createMaterial({
212
+ baseColor,
213
+ metallic,
214
+ roughness,
215
+ refractionIndex: ior,
216
+ reflectance,
217
+ emissiveColor,
218
+ albedoTextureId: baseColorTextureId,
219
+ normalTextureId,
220
+ normalScale: mat.normalTexture?.scale,
221
+ metallicRoughnessTextureId,
222
+ emissiveTextureId,
223
+ occlusionTextureId,
224
+ occlusionStrength: mat.occlusionTexture?.strength,
225
+ textureUvSets: [
226
+ pbr?.baseColorTexture ?? specGloss?.diffuseTexture,
227
+ mat.normalTexture,
228
+ pbr?.metallicRoughnessTexture,
229
+ mat.occlusionTexture,
230
+ mat.emissiveTexture
231
+ ].map((info) => (info?.texCoord === 1 ? 'uv1' : 'uv0')),
232
+ side: mat.doubleSided ? 'double' : 'front',
233
+ surfaceType,
234
+ alphaCutoff
235
+ });
236
+
237
+ materialMap.set(mIdx, materialId);
238
+ createdMaterialIds.push(materialId);
239
+ }
240
+ }
241
+
242
+ // 3. Upload Geometries
243
+ if (gltf.meshes) {
244
+ for (let meshIdx = 0; meshIdx < gltf.meshes.length; meshIdx++) {
245
+ const mesh = gltf.meshes[meshIdx]!;
246
+
247
+ for (let primIdx = 0; primIdx < mesh.primitives.length; primIdx++) {
248
+ const prim: GlTFPrimitive = mesh.primitives[primIdx]!;
249
+ let positions: Float32Array;
250
+ let normals: Float32Array | undefined;
251
+ let uvs: Float32Array | undefined;
252
+ let uv1: Float32Array | undefined;
253
+ let tangents: Float32Array | undefined;
254
+ let colors: Float32Array | undefined;
255
+ let joints0: Uint32Array | Uint16Array | undefined;
256
+ let weights0: Float32Array | undefined;
257
+ let joints1: Uint32Array | Uint16Array | undefined;
258
+ let weights1: Float32Array | undefined;
259
+ let indices: Uint32Array | Uint16Array | undefined;
260
+
261
+ // Check for Draco mesh compression extension
262
+ if (prim.extensions?.KHR_draco_mesh_compression && decoders.draco) {
263
+ const dracoExt = prim.extensions.KHR_draco_mesh_compression;
264
+ const bv = gltf.bufferViews?.[dracoExt.bufferView];
265
+ if (bv) {
266
+ const buffer = buffers[bv.buffer];
267
+ if (buffer) {
268
+ const start = bv.byteOffset ?? 0;
269
+ const dracoBytes = buffer.subarray(start, start + bv.byteLength);
270
+ const decoded = await decoders.draco.decode(dracoBytes, dracoExt.attributes);
271
+ positions = decoded.position ?? new Float32Array(0);
272
+ normals = decoded.normal;
273
+ uvs = decoded.uv;
274
+ uv1 = decoded.uv1;
275
+ tangents = decoded.tangent;
276
+ colors = decoded.color;
277
+ joints0 = decoded.joints;
278
+ weights0 = decoded.weights;
279
+ indices = decoded.indices;
280
+ } else {
281
+ positions = new Float32Array(0);
282
+ }
283
+ } else {
284
+ positions = new Float32Array(0);
285
+ }
286
+ } else {
287
+ // Standard uncompressed accessors
288
+ const posAcc = prim.attributes['POSITION'];
289
+ if (posAcc === undefined) continue;
290
+ positions = readAccessor(gltf, buffers, posAcc) as Float32Array;
291
+
292
+ if (prim.attributes['NORMAL'] !== undefined) {
293
+ normals = readAccessor(gltf, buffers, prim.attributes['NORMAL']) as Float32Array;
294
+ }
295
+ if (prim.attributes['TEXCOORD_0'] !== undefined) {
296
+ uvs = readAccessor(gltf, buffers, prim.attributes['TEXCOORD_0']) as Float32Array;
297
+ }
298
+ if (prim.attributes['TEXCOORD_1'] !== undefined) {
299
+ uv1 = readAccessor(gltf, buffers, prim.attributes['TEXCOORD_1']) as Float32Array;
300
+ }
301
+ if (prim.attributes['TANGENT'] !== undefined) {
302
+ tangents = readAccessor(gltf, buffers, prim.attributes['TANGENT']) as Float32Array;
303
+ }
304
+ if (prim.attributes['COLOR_0'] !== undefined) {
305
+ colors = readAccessor(gltf, buffers, prim.attributes['COLOR_0']) as Float32Array;
306
+ if (gltf.accessors?.[prim.attributes['COLOR_0']]?.type === 'VEC3') {
307
+ const rgba = new Float32Array((colors.length / 3) * 4);
308
+ for (let i = 0; i < colors.length / 3; i++) {
309
+ rgba.set(colors.subarray(i * 3, i * 3 + 3), i * 4);
310
+ rgba[i * 4 + 3] = 1;
311
+ }
312
+ colors = rgba;
313
+ }
314
+ }
315
+ if (prim.attributes['JOINTS_0'] !== undefined) {
316
+ joints0 = readAccessor(gltf, buffers, prim.attributes['JOINTS_0']) as
317
+ Uint32Array | Uint16Array;
318
+ }
319
+ if (prim.attributes['WEIGHTS_0'] !== undefined) {
320
+ weights0 = readAccessor(gltf, buffers, prim.attributes['WEIGHTS_0']) as Float32Array;
321
+ }
322
+ if (prim.attributes['JOINTS_1'] !== undefined) {
323
+ joints1 = readAccessor(gltf, buffers, prim.attributes['JOINTS_1']) as
324
+ Uint32Array | Uint16Array;
325
+ }
326
+ if (prim.attributes['WEIGHTS_1'] !== undefined) {
327
+ weights1 = readAccessor(gltf, buffers, prim.attributes['WEIGHTS_1']) as Float32Array;
328
+ }
329
+ if (prim.indices !== undefined) {
330
+ indices = readAccessor(gltf, buffers, prim.indices) as Uint32Array | Uint16Array;
331
+ }
332
+ }
333
+
334
+ if (prim.mode === 2 || prim.mode === 6) {
335
+ const src = indices ?? Uint32Array.from({ length: positions.length / 3 }, (_, i) => i);
336
+ if (prim.mode === 2) indices = Uint32Array.from([...src, src[0]!]);
337
+ else {
338
+ const triangles: number[] = [];
339
+ for (let i = 1; i + 1 < src.length; i++) triangles.push(src[0]!, src[i]!, src[i + 1]!);
340
+ indices = new Uint32Array(triangles);
341
+ }
342
+ }
343
+ const { geometryId } = realm.createGeometry({
344
+ positions,
345
+ normals,
346
+ uvs,
347
+ uv1,
348
+ tangents,
349
+ colors,
350
+ joints0,
351
+ weights0,
352
+ joints1,
353
+ weights1,
354
+ indices,
355
+ topology: mapGltfPrimitiveTopology(prim.mode)
356
+ });
357
+
358
+ if (prim.targets?.length) {
359
+ const targets = prim.targets.map((target) => ({
360
+ positions:
361
+ target['POSITION'] === undefined
362
+ ? undefined
363
+ : (readAccessor(gltf, buffers, target['POSITION']) as Float32Array),
364
+ normals:
365
+ target['NORMAL'] === undefined
366
+ ? undefined
367
+ : (readAccessor(gltf, buffers, target['NORMAL']) as Float32Array),
368
+ tangents:
369
+ target['TANGENT'] === undefined
370
+ ? undefined
371
+ : (readAccessor(gltf, buffers, target['TANGENT']) as Float32Array)
372
+ }));
373
+ realm.setGeometryMorphTargets(geometryId, targets, positions.length / 3);
374
+ }
375
+
376
+ const key = `${meshIdx}_${primIdx}`;
377
+ geometryMap.set(key, geometryId);
378
+ createdGeometryIds.push(geometryId);
379
+ }
380
+ }
381
+ }
382
+
383
+ return {
384
+ geometryMap,
385
+ materialMap,
386
+ textureMap,
387
+ createdGeometryIds,
388
+ createdMaterialIds,
389
+ createdTextureIds
390
+ };
391
+ }