webgpu-pt

monte carlo path tracer

gltf.ts

15 kB
  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}