1import { load } from "@loaders.gl/core";
2import { GLTFImagePostprocessed, GLTFLoader, GLTFMeshPrimitivePostprocessed, GLTFPostprocessed, postProcessGLTF } from "@loaders.gl/gltf";
3import { Mat3, Mat4, Mat4Like, Quat, QuatLike, Vec3, Vec3Like } from "gl-matrix";
4
5interface Triangle {
6 centroid: number[];
7 cornerA: number[];
8 cornerB: number[];
9 cornerC: number[];
10 normalA: number[];
11 normalB: number[];
12 normalC: number[];
13 mat: number;
14 uvA: number[];
15 uvB: number[];
16 uvC: number[];
17 tangentA: number[];
18 tangentB: number[];
19 tangentC: number[];
20}
21
22interface ProcessedMaterial {
23 baseColorFactor: number[],
24 baseColorTexture: number, // idx
25 metallicFactor: number,
26 roughnessFactor: number,
27 metallicRoughnessTexture: number, //idx
28 normalTexture: number, //idx
29 emissiveFactor: number[],
30 emissiveTexture: number, //idx
31 alphaMode: number, // parseAlphaMode
32 alphaCutoff: number,
33 doubleSided: number,
34};
35
36interface ProcessedTexture {
37 id: String,
38 sampler: GPUSampler,
39 texture: GPUTexture,
40 view: GPUTextureView,
41 source: GLTFImagePostprocessed,
42 samplerDescriptor: GPUSamplerDescriptor,
43}
44
45export class GLTF2 {
46 triangles: Array<Triangle>;
47 materials: Array<ProcessedMaterial>; // could make material more explicit here, like with textures
48 textures: Array<ProcessedTexture>;
49 device: GPUDevice;
50 gltfData!: GLTFPostprocessed;
51 url: string;
52 scale: number[];
53 position: number[];
54 rotation?: number[];
55
56 // pre allocating these here, faster that way? Intuitevely, I could be wrong.
57 tempVec3_0: Vec3 = Vec3.create();
58 tempVec3_1: Vec3 = Vec3.create();
59 tempVec3_2: Vec3 = Vec3.create();
60 tempVec3_3: Vec3 = Vec3.create();
61 tempVec3_4: Vec3 = Vec3.create();
62 tempMat4_0: Mat4 = Mat4.create();
63 tempMat4_1: Mat4 = Mat4.create();
64 tempMat3_0: Mat3 = Mat3.create();
65
66 constructor(device: GPUDevice, url: string, scale: number[], position: number[], rotation?: number[]) {
67 this.triangles = [];
68 this.materials = [];
69 this.textures = [];
70 this.device = device;
71 this.url = url;
72 this.scale = scale;
73 this.position = position;
74 this.rotation = rotation;
75 }
76 async initialize() {
77 const t = await load(this.url, GLTFLoader);
78 this.gltfData = postProcessGLTF(t);
79 this.traverseNodes();
80 return [this.triangles, this.materials, this.textures];
81 }
82 // some data swizzling swash buckling utils
83 transformVec3(inputVec: ArrayLike<number>, matrix: Mat4): number[] {
84 const v = this.tempVec3_0;
85 Vec3.set(v, inputVec[0], inputVec[1], inputVec[2]);
86 Vec3.transformMat4(v, v, matrix);
87 return [v[0], v[1], v[2]]; // Return new array copy
88 }
89 transformNormal(inputNormal: ArrayLike<number>, transformMatrix: Mat4): number[] {
90 const normalMatrix = this.tempMat3_0; // Reused Mat3
91 const tempMatrix = this.tempMat4_1; // Use tempMat4_1 to avoid conflict with finalTransform
92 const transformedNormal = this.tempVec3_0; // Reused Vec3 for result
93 const inputNormalVec = this.tempVec3_1; // Reused Vec3 for input
94
95 // calculate transpose(invert(transformMatrix))
96 // tempMat4_1 as scratch space to avoid clobbering tempMat4_0 (finalTransform)
97 Mat4.invert(tempMatrix, transformMatrix);
98 Mat4.transpose(tempMatrix, tempMatrix);
99
100 // upper-left 3x3 submatrix
101 Mat3.fromMat4(normalMatrix, tempMatrix);
102
103 // normal into reusable Vec3
104 Vec3.set(inputNormalVec, inputNormal[0], inputNormal[1], inputNormal[2]);
105
106 // transfrom that normal
107 Vec3.transformMat3(transformedNormal, inputNormalVec, normalMatrix);
108 Vec3.normalize(transformedNormal, transformedNormal);
109
110 // new array copy
111 return [transformedNormal[0], transformedNormal[1], transformedNormal[2]];
112 }
113 parseAlphaMode(alphaMode: string) {
114 if (alphaMode === "MASK") { return 1 }
115 return 2
116 }
117
118
119 // could break this up
120 extractTriangles(primitive: GLTFMeshPrimitivePostprocessed, transform: Mat4, targetArray: Array<Triangle>) {
121 const positions = primitive.attributes["POSITION"].value;
122 const indicesData = primitive.indices ? primitive.indices.value : null;
123 const numVertices = positions.length / 3;
124 const indices = indicesData ?? (() => {
125 const generatedIndices = new Uint32Array(numVertices);
126 for (let i = 0; i < numVertices; i++) generatedIndices[i] = i;
127 return generatedIndices;
128 })();
129 const normals = primitive.attributes["NORMAL"]
130 ? primitive.attributes["NORMAL"].value
131 : null;
132 const uvCoords = primitive.attributes["TEXCOORD_0"]
133 ? primitive.attributes["TEXCOORD_0"].value
134 : null;
135 const tangents = primitive.attributes["TANGENT"]
136 ? primitive.attributes["TANGENT"].value
137 : null;
138
139 const mat = parseInt(primitive.material?.id.match(/\d+$/)?.[0] ?? "-1");
140
141 // ensure these don't clash with temps used in transformNormal/transformVec3
142 // if called within the face normal logic (they aren't).
143 const vA = this.tempVec3_1; // maybe use distinct temps if needed, but seems ok
144 const vB = this.tempVec3_2;
145 const vC = this.tempVec3_3;
146 const edge1 = this.tempVec3_4;
147 const edge2 = this.tempVec3_0; // tempVec3_0 reused safely after position transforms
148 const faceNormal = this.tempVec3_1; // tempVec3_1 reused safely after normal transforms or for input
149
150 const defaultUV = [0, 0];
151 const defaultTangent = [1, 0, 0, 1];
152
153 for (let i = 0; i < indices.length; i += 3) {
154 const ai = indices[i];
155 const bi = indices[i + 1];
156 const ci = indices[i + 2];
157
158 const posA = [positions[ai * 3], positions[ai * 3 + 1], positions[ai * 3 + 2]];
159 const posB = [positions[bi * 3], positions[bi * 3 + 1], positions[bi * 3 + 2]];
160 const posC = [positions[ci * 3], positions[ci * 3 + 1], positions[ci * 3 + 2]];
161
162 // transform positions uses tempVec3_0 internally
163 const cornerA = this.transformVec3(posA, transform);
164 const cornerB = this.transformVec3(posB, transform);
165 const cornerC = this.transformVec3(posC, transform);
166
167 let normalA: number[], normalB: number[], normalC: number[];
168 if (normals) {
169 // transform normals uses tempVec3_0, tempVec3_1 internally
170 normalA = this.transformNormal([normals[ai * 3], normals[ai * 3 + 1], normals[ai * 3 + 2]], transform);
171 normalB = this.transformNormal([normals[bi * 3], normals[bi * 3 + 1], normals[bi * 3 + 2]], transform);
172 normalC = this.transformNormal([normals[ci * 3], normals[ci * 3 + 1], normals[ci * 3 + 2]], transform);
173 } else {
174 // compute fallback flat face normal
175 Vec3.set(vA, cornerA[0], cornerA[1], cornerA[2]);
176 Vec3.set(vB, cornerB[0], cornerB[1], cornerB[2]);
177 Vec3.set(vC, cornerC[0], cornerC[1], cornerC[2]);
178
179 Vec3.subtract(edge1, vB, vA);
180 Vec3.subtract(edge2, vC, vA);
181 Vec3.cross(faceNormal, edge1, edge2);
182 Vec3.normalize(faceNormal, faceNormal);
183
184 const normalArray = [faceNormal[0], faceNormal[1], faceNormal[2]];
185 normalA = normalArray;
186 normalB = normalArray;
187 normalC = normalArray;
188 }
189
190 const uvA = uvCoords ? [uvCoords[ai * 2], uvCoords[ai * 2 + 1]] : defaultUV;
191 const uvB = uvCoords ? [uvCoords[bi * 2], uvCoords[bi * 2 + 1]] : defaultUV;
192 const uvC = uvCoords ? [uvCoords[ci * 2], uvCoords[ci * 2 + 1]] : defaultUV;
193
194 const tangentA = tangents ? [tangents[ai * 4], tangents[ai * 4 + 1], tangents[ai * 4 + 2], tangents[ai * 4 + 3]] : defaultTangent;
195 const tangentB = tangents ? [tangents[bi * 4], tangents[bi * 4 + 1], tangents[bi * 4 + 2], tangents[bi * 4 + 3]] : defaultTangent;
196 const tangentC = tangents ? [tangents[ci * 4], tangents[ci * 4 + 1], tangents[ci * 4 + 2], tangents[ci * 4 + 3]] : defaultTangent;
197
198 const centroid = [
199 (cornerA[0] + cornerB[0] + cornerC[0]) / 3,
200 (cornerA[1] + cornerB[1] + cornerC[1]) / 3,
201 (cornerA[2] + cornerB[2] + cornerC[2]) / 3,
202 ];
203
204 targetArray.push({
205 centroid,
206 cornerA, cornerB, cornerC,
207 normalA, normalB, normalC, mat,
208 uvA, uvB, uvC,
209 tangentA, tangentB, tangentC,
210 });
211 }
212 }
213
214 traverseNodes() {
215 // texture processing
216 if (this.gltfData.textures) {
217 this.gltfData.textures.forEach((texture) => {
218 if (!texture.source?.image) {
219 // empty textures are handled on atlas creation
220 return;
221 }
222 const gpuTexture = this.device.createTexture({
223 size: {
224 width: texture.source.image.width ?? 0,
225 height: texture.source.image.height ?? 0,
226 depthOrArrayLayers: 1,
227 },
228 format: "rgba8unorm",
229 usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
230 });
231
232 const view = gpuTexture.createView({ format: "rgba8unorm" });
233
234 // TODO: Process gltfData.samplers[texture.sampler] if it exists
235 let samplerDescriptor: GPUSamplerDescriptor = {
236 magFilter: "linear", minFilter: "linear",
237 addressModeU: "repeat", addressModeV: "repeat",
238 };
239 const sampler = this.device.createSampler(samplerDescriptor);
240 this.textures.push({
241 id: texture.id,
242 texture: gpuTexture,
243 view: view,
244 sampler: sampler,
245 source: texture.source,
246 samplerDescriptor: samplerDescriptor
247 });
248 });
249 }
250 if (this.gltfData.materials) {
251 this.materials = this.gltfData.materials.map(mat => {
252 return {
253 baseColorFactor: mat.pbrMetallicRoughness?.baseColorFactor ?? [1.0, 1.0, 1.0, 1.0],
254 baseColorTexture: mat.pbrMetallicRoughness?.baseColorTexture?.index ?? -1,
255 metallicFactor: mat.pbrMetallicRoughness?.metallicFactor ?? 1.0,
256 roughnessFactor: mat.pbrMetallicRoughness?.roughnessFactor ?? 1.0,
257 metallicRoughnessTexture: mat.pbrMetallicRoughness?.metallicRoughnessTexture?.index ?? -1,
258 normalTexture: mat.normalTexture?.index ?? -1,
259 emissiveFactor: mat.emissiveFactor ?? [0.0, 0.0, 0.0],
260 emissiveTexture: mat.emissiveTexture?.index ?? -1,
261 alphaMode: mat.alphaMode ? this.parseAlphaMode(mat.alphaMode) : 0,
262 alphaCutoff: mat.alphaCutoff ?? 0.5,
263 doubleSided: mat.doubleSided ? 1 : 0,
264 };
265 });
266 }
267
268 // initial node transforms
269 const finalTransform = this.tempMat4_0; // reused for final calc per node
270 const nodeLocalTransform = this.tempMat4_1; // reused for local calc per node
271 const tMat = Mat4.create();
272 const rMat = Mat4.create();
273 const sMat = Mat4.create();
274 const tMatCustom = Mat4.create();
275 const rMatCustom = Mat4.create();
276 const sMatCustom = Mat4.create();
277 const yToZUp = Mat4.fromValues(
278 1, 0, 0, 0,
279 0, 0, 1, 0,
280 0, -1, 0, 0,
281 0, 0, 0, 1
282 );
283 // scene transforms
284 const sceneTransform = Mat4.create();
285 const sc_translation = this.position || [0, 0, 0];
286 const sc_rotation = this.rotation || [0, 0, 0, 1];
287 const sc_scale = this.scale || [1, 1, 1];
288 Mat4.fromTranslation(tMatCustom, sc_translation as Vec3Like);
289 Quat.normalize(rMatCustom, sc_rotation as QuatLike);
290 Mat4.fromQuat(rMatCustom, rMatCustom);
291 Mat4.fromScaling(sMatCustom, sc_scale as Vec3Like);
292 Mat4.multiply(sceneTransform, rMatCustom, sMatCustom);
293 Mat4.multiply(sceneTransform, tMatCustom, sceneTransform);
294
295 const meshMap = new Map(this.gltfData.meshes.map(m => [m.id, m]));
296
297 for (const node of this.gltfData.nodes) {
298 if (!node.mesh?.id) continue;
299 const mesh = meshMap.get(node.mesh.id);
300 if (!mesh) continue;
301 Mat4.identity(nodeLocalTransform);
302 if (node.matrix) {
303 Mat4.copy(nodeLocalTransform, node.matrix as Mat4Like);
304 } else {
305 const nodeTranslation = node.translation || [0, 0, 0];
306 const nodeRotation = node.rotation || [0, 0, 0, 1];
307 const nodeScale = node.scale || [1, 1, 1];
308 Mat4.fromTranslation(tMat, nodeTranslation as Vec3Like);
309 Mat4.fromQuat(rMat, nodeRotation as QuatLike);
310 Mat4.fromScaling(sMat, nodeScale as Vec3Like);
311 Mat4.multiply(nodeLocalTransform, rMat, sMat);
312 Mat4.multiply(nodeLocalTransform, tMat, nodeLocalTransform);
313 }
314
315 // finalTransform = sceneTransform * yToZUp * nodeLocalTransform
316 Mat4.multiply(finalTransform, yToZUp, nodeLocalTransform);
317 Mat4.multiply(finalTransform, sceneTransform, finalTransform);
318
319 mesh.primitives.forEach((primitive: GLTFMeshPrimitivePostprocessed) => {
320 this.extractTriangles(primitive, finalTransform, this.triangles);
321 });
322 }
323 }
324}
325
326export function combineGLTFs(gltfs: GLTF2[]) {
327 const triangles = [];
328 const materials = [];
329 const textures = [];
330
331 let textureOffset = 0;
332 let materialOffset = 0;
333 let largestTextureDimensions = { width: 0, height: 0 };
334
335 // offset idx
336 const offsetIdx = (idx: any) => {
337 return (typeof idx === 'number' && idx >= 0) ? idx + textureOffset : idx; // Keep original if invalid index
338 };
339
340 for (let i = 0; i < gltfs.length; i++) {
341 const gltf = gltfs[i];
342 const texCount = gltf.textures ? gltf.textures.length : 0;
343 const matCount = gltf.materials ? gltf.materials.length : 0;
344 // just append the textures for now
345 if (gltf.textures && texCount > 0) {
346 for (let t = 0; t < texCount; t++) {
347 const texture = gltf.textures[t];
348 let texHeight = texture.source.image.height as number;
349 let texWidth = texture.source.image.width as number;
350 textures.push(texture);
351 if (texWidth > largestTextureDimensions.width) {
352 largestTextureDimensions.width = texWidth;
353 }
354 if (texHeight > largestTextureDimensions.height) {
355 largestTextureDimensions.height = texHeight;
356 }
357 }
358 }
359 if (gltf.materials && matCount > 0) {
360 for (let m = 0; m < matCount; m++) {
361 const src = gltf.materials[m];
362 materials.push({
363 alphaCutoff: src.alphaCutoff,
364 alphaMode: src.alphaMode,
365 baseColorFactor: src.baseColorFactor,
366 baseColorTexture: offsetIdx(src.baseColorTexture),
367 doubleSided: src.doubleSided,
368 emissiveFactor: src.emissiveFactor,
369 emissiveTexture: offsetIdx(src.emissiveTexture),
370 metallicFactor: src.metallicFactor,
371 metallicRoughnessTexture: offsetIdx(src.metallicRoughnessTexture),
372 normalTexture: offsetIdx(src.normalTexture),
373 roughnessFactor: src.roughnessFactor
374 });
375 }
376 }
377 // update idx if needed
378 if (gltf.triangles) {
379 for (let t = 0; t < gltf.triangles.length; t++) {
380 const tri = gltf.triangles[t];
381 const triCopy = Object.create(tri);
382 if (tri.mat >= 0) {
383 triCopy.mat = tri.mat + materialOffset;
384 } else {
385 triCopy.mat = -1;
386 }
387 triangles.push(triCopy);
388 }
389 }
390 textureOffset += texCount;
391 materialOffset += matCount;
392 }
393 return { triangles, materials, textures, largestTextureDimensions };
394}