docs: function loadObj(objPath, options) {
      // Global store of vertex attributes listed in the obj file
      let positions = new ArrayStorage(ComponentDatatype.FLOAT);
      let normals = new ArrayStorage(ComponentDatatype.FLOAT);
      let uvs = new ArrayStorage(ComponentDatatype.FLOAT);
  
      // The current node, mesh, and primitive
      let node;
      let mesh;
      let primitive;
      let activeMaterial;
  
      // All nodes seen in the obj
      const nodes = [];
  
      // Used to build the indices. The vertex cache is unique to each primitive.
      let vertexCache = {};
      const vertexCacheLimit = 1000000;
      let vertexCacheCount = 0;
      let vertexCount = 0;
  
      // All mtl paths seen in the obj
      let mtlPaths = [];
  
      // Buffers for face data that spans multiple lines
      let lineBuffer = '';
  
      // Used for parsing face data
      const faceVertices = [];
      const facePositions = [];
      const faceUvs = [];
      const faceNormals = [];
  
      function clearVertexCache() {
          vertexCache = {};
          vertexCacheCount = 0;
      }
  
      function getName(name) {
          return (name === '' ? undefined : name);
      }
  
      function addNode(name) {
          node = new Node();
          node.name = getName(name);
          nodes.push(node);
          addMesh();
      }
  
      function addMesh(name) {
          mesh = new Mesh();
          mesh.name = getName(name);
          node.meshes.push(mesh);
          addPrimitive();
      }
  
      function addPrimitive() {
          primitive = new Primitive();
          primitive.material = activeMaterial;
          mesh.primitives.push(primitive);
  
          // Clear the vertex cache for each new primitive
          clearVertexCache();
          vertexCount = 0;
      }
  
      function reusePrimitive(callback) {
          const primitives = mesh.primitives;
          const primitivesLength = primitives.length;
          for (let i = 0; i < primitivesLength; ++i) {
              if (primitives[i].material === activeMaterial) {
                  if (!defined(callback) || callback(primitives[i])) {
                      primitive = primitives[i];
                      clearVertexCache();
                      vertexCount = primitive.positions.length / 3;
                      return;
                  }
              }
          }
          addPrimitive();
      }
  
      function useMaterial(name) {
          activeMaterial = getName(name);
          reusePrimitive();
      }
  
      function faceAndPrimitiveMatch(uvs, normals, primitive) {
          const faceHasUvs = uvs[0].length > 0;
          const faceHasNormals = normals[0].length > 0;
          const primitiveHasUvs = primitive.uvs.length > 0;
          const primitiveHasNormals = primitive.normals.length > 0;
          return primitiveHasUvs === faceHasUvs && primitiveHasNormals === faceHasNormals;
      }
  
      function checkPrimitive(uvs, normals) {
          const firstFace = primitive.indices.length === 0;
          if (!firstFace && !faceAndPrimitiveMatch(uvs, normals, primitive)) {
              reusePrimitive(function(primitive) {
                  return faceAndPrimitiveMatch(uvs, normals, primitive);
              });
          }
      }
  
      function getOffset(a, attributeData, components) {
          const i = parseInt(a);
          if (i < 0) {
              // Negative vertex indexes reference the vertices immediately above it
              return (attributeData.length / components + i) * components;
          }
          return (i - 1) * components;
      }
  
      function createVertex(p, u, n) {
          // Positions
          if (p.length > 0) {
              const pi = getOffset(p, positions, 3);
              const px = positions.get(pi + 0);
              const py = positions.get(pi + 1);
              const pz = positions.get(pi + 2);
              primitive.positions.push(px);
              primitive.positions.push(py);
              primitive.positions.push(pz);
          }
  
          // Normals
          if (n.length > 0) {
              const ni = getOffset(n, normals, 3);
              const nx = normals.get(ni + 0);
              const ny = normals.get(ni + 1);
              const nz = normals.get(ni + 2);
              primitive.normals.push(nx);
              primitive.normals.push(ny);
              primitive.normals.push(nz);
          }
  
          // UVs
          if (u.length > 0) {
              const ui = getOffset(u, uvs, 2);
              const ux = uvs.get(ui + 0);
              const uy = uvs.get(ui + 1);
              primitive.uvs.push(ux);
              primitive.uvs.push(uy);
          }
      }
  
      function addVertex(v, p, u, n) {
          let index = vertexCache[v];
          if (!defined(index)) {
              index = vertexCount++;
              vertexCache[v] = index;
              createVertex(p, u, n);
  
              // Prevent the vertex cache from growing too large. As a result of clearing the cache there
              // may be some duplicate vertices.
              vertexCacheCount++;
              if (vertexCacheCount > vertexCacheLimit) {
                  clearVertexCache();
              }
          }
          return index;
      }
  
      function getPosition(index, result) {
          const pi = getOffset(index, positions, 3);
          const px = positions.get(pi + 0);
          const py = positions.get(pi + 1);
          const pz = positions.get(pi + 2);
          return Cartesian3.fromElements(px, py, pz, result);
      }
  
      function getNormal(index, result) {
          const ni = getOffset(index, normals, 3);
          const nx = normals.get(ni + 0);
          const ny = normals.get(ni + 1);
          const nz = normals.get(ni + 2);
          return Cartesian3.fromElements(nx, ny, nz, result);
      }
  
      const scratch1 = new Cartesian3();
      const scratch2 = new Cartesian3();
      const scratch3 = new Cartesian3();
      const scratch4 = new Cartesian3();
      const scratch5 = new Cartesian3();
      const scratchCenter = new Cartesian3();
      const scratchAxis1 = new Cartesian3();
      const scratchAxis2 = new Cartesian3();
      const scratchNormal = new Cartesian3();
      const scratchPositions = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()];
      const scratchVertexIndices = [];
      const scratchPoints = [];
  
      function checkWindingCorrect(positionIndex1, positionIndex2, positionIndex3, normalIndex) {
          if (normalIndex.length === 0) {
              // If no face normal, we have to assume the winding is correct.
              return true;
          }
          const normal = getNormal(normalIndex, scratchNormal);
          const A = getPosition(positionIndex1, scratch1);
          const B = getPosition(positionIndex2, scratch2);
          const C = getPosition(positionIndex3, scratch3);
  
          const BA = Cartesian3.subtract(B, A, scratch4);
          const CA = Cartesian3.subtract(C, A, scratch5);
          const cross = Cartesian3.cross(BA, CA, scratch3);
  
          return (Cartesian3.dot(normal, cross) >= 0);
      }
  
      function addTriangle(index1, index2, index3, correctWinding) {
          if (correctWinding) {
              primitive.indices.push(index1);
              primitive.indices.push(index2);
              primitive.indices.push(index3);
          } else {
              primitive.indices.push(index1);
              primitive.indices.push(index3);
              primitive.indices.push(index2);
          }
      }
  
      function addFace(vertices, positions, uvs, normals) {
          checkPrimitive(uvs, normals);
  
          if (vertices.length === 3) {
              const isWindingCorrect = checkWindingCorrect(positions[0], positions[1], positions[2], normals[0]);
              const index1 = addVertex(vertices[0], positions[0], uvs[0], normals[0]);
              const index2 = addVertex(vertices[1], positions[1], uvs[1], normals[1]);
              const index3 = addVertex(vertices[2], positions[2], uvs[2], normals[2]);
              addTriangle(index1, index2, index3, isWindingCorrect);
          } else { // Triangulate if the face is not a triangle
              const points = scratchPoints;
              const vertexIndices = scratchVertexIndices;
  
              points.length = 0;
              vertexIndices.length = 0;
  
              for (let i = 0; i < vertices.length; ++i) {
                  const index = addVertex(vertices[i], positions[i], uvs[i], normals[i]);
                  vertexIndices.push(index);
                  if (i === scratchPositions.length) {
                      scratchPositions.push(new Cartesian3());
                  }
                  points.push(getPosition(positions[i], scratchPositions[i]));
              }
  
              const validGeometry = CoplanarPolygonGeometryLibrary.computeProjectTo2DArguments(points, scratchCenter, scratchAxis1, scratchAxis2);
              if (!validGeometry) {
                  return;
              }
              const projectPoints = CoplanarPolygonGeometryLibrary.createProjectPointsTo2DFunction(scratchCenter, scratchAxis1, scratchAxis2);
              const points2D = projectPoints(points);
              const indices = PolygonPipeline.triangulate(points2D);
              const isWindingCorrect = PolygonPipeline.computeWindingOrder2D(points2D) !== WindingOrder.CLOCKWISE;
  
              for (let i = 0; i < indices.length - 2; i += 3) {
                  addTriangle(vertexIndices[indices[i]], vertexIndices[indices[i+1]], vertexIndices[indices[i+2]], isWindingCorrect);
              }
          }
      }
  
      function parseLine(line) {
          line = line.trim();
          let result;
  
          if ((line.length === 0) || (line.charAt(0) === '#')) {
              // Don't process empty lines or comments
          } else if (/^o\s/i.test(line)) {
              const objectName = line.substring(2).trim();
              addNode(objectName);
          } else if (/^g\s/i.test(line)) {
              const groupName = line.substring(2).trim();
              addMesh(groupName);
          } else if (/^usemtl/i.test(line)) {
              const materialName = line.substring(7).trim();
              useMaterial(materialName);
          } else if (/^mtllib/i.test(line)) {
              const mtllibLine = line.substring(7).trim();
              mtlPaths = mtlPaths.concat(getMtlPaths(mtllibLine));
          } else if ((result = vertexPattern.exec(line)) !== null) {
              positions.push(parseFloat(result[1]));
              positions.push(parseFloat(result[2]));
              positions.push(parseFloat(result[3]));
          } else if ((result = normalPattern.exec(line) ) !== null) {
              const normal = Cartesian3.fromElements(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]), scratchNormal);
              if (Cartesian3.equals(normal, Cartesian3.ZERO)) {
                  Cartesian3.clone(Cartesian3.UNIT_Z, normal);
              } else {
                  Cartesian3.normalize(normal, normal);
              }
              normals.push(normal.x);
              normals.push(normal.y);
              normals.push(normal.z);
          } else if ((result = uvPattern.exec(line)) !== null) {
              uvs.push(parseFloat(result[1]));
              uvs.push(1.0 - parseFloat(result[2])); // Flip y so 0.0 is the bottom of the image
          } else { // face line or invalid line
              // Because face lines can contain n vertices, we use a line buffer in case the face data spans multiple lines.
              // If there's a line continuation don't create face yet
              if (line.slice(-1) === '\\') {
                  lineBuffer += line.substring(0, line.length-1);
                  return;
              }
              lineBuffer += line;
              if (lineBuffer.substring(0, 2) === 'f ') {
                  while ((result = facePattern.exec(lineBuffer)) !== null) {
                      faceVertices.push(result[0]);
                      facePositions.push(result[1]);
                      faceUvs.push(result[2]);
                      faceNormals.push(result[3]);
                  }
                  if (faceVertices.length > 2) {
                      addFace(faceVertices, facePositions, faceUvs, faceNormals);
                  }
  
                  faceVertices.length = 0;
                  facePositions.length = 0;
                  faceNormals.length = 0;
                  faceUvs.length = 0;
              }
              lineBuffer = '';
          }
      }
  
      // Create a default node in case there are no o/g/usemtl lines in the obj
      addNode();
  
      // Parse the obj file
      return readLines(objPath, parseLine)
          .then(function() {
              // Add hasNormals to options object for loadMtl
              options.hasNormals = normals.length > 0;
  
              // Unload resources
              positions = undefined;
              normals = undefined;
              uvs = undefined;
  
              // Load materials and textures
              return finishLoading(nodes, mtlPaths, objPath, defined(activeMaterial), options);
          });
  }