UNPKG

webgl-obj-loader

Version:

A simple OBJ model loader to help facilitate the learning of WebGL

781 lines (701 loc) 37.4 kB
import { Layout } from "./layout"; import { Material, MaterialLibrary } from "./material"; export interface MeshOptions { enableWTextureCoord?: boolean; calcTangentsAndBitangents?: boolean; materials?: { [key: string]: Material }; } interface UnpackedAttrs { verts: number[]; norms: number[]; textures: number[]; hashindices: { [k: string]: number }; indices: number[][]; materialIndices: number[]; index: number; } export interface MaterialNameToIndex { [k: string]: number; } export interface IndexToMaterial { [k: number]: Material; } export interface ArrayBufferWithItemSize extends ArrayBuffer { numItems?: number; } export interface Uint16ArrayWithItemSize extends Uint16Array { numItems?: number; } /** * The main Mesh class. The constructor will parse through the OBJ file data * and collect the vertex, vertex normal, texture, and face information. This * information can then be used later on when creating your VBOs. See * OBJ.initMeshBuffers for an example of how to use the newly created Mesh */ export default class Mesh { public vertices: number[]; public vertexNormals: number[]; public textures: number[]; public indices: number[]; public name: string = ""; public vertexMaterialIndices: number[]; public indicesPerMaterial: number[][] = []; public materialNames: string[]; public materialIndices: MaterialNameToIndex; public materialsByIndex: IndexToMaterial = {}; public tangents: number[] = []; public bitangents: number[] = []; public textureStride: number; /** * Create a Mesh * @param {String} objectData - a string representation of an OBJ file with * newlines preserved. * @param {Object} options - a JS object containing valid options. See class * documentation for options. * @param {bool} options.enableWTextureCoord - Texture coordinates can have * an optional "w" coordinate after the u and v coordinates. This extra * value can be used in order to perform fancy transformations on the * textures themselves. Default is to truncate to only the u an v * coordinates. Passing true will provide a default value of 0 in the * event that any or all texture coordinates don't provide a w value. * Always use the textureStride attribute in order to determine the * stride length of the texture coordinates when rendering the element * array. * @param {bool} options.calcTangentsAndBitangents - Calculate the tangents * and bitangents when loading of the OBJ is completed. This adds two new * attributes to the Mesh instance: `tangents` and `bitangents`. */ constructor(objectData: string, options?: MeshOptions) { options = options || {}; options.materials = options.materials || {}; options.enableWTextureCoord = !!options.enableWTextureCoord; // the list of unique vertex, normal, texture, attributes this.vertexNormals = []; this.textures = []; // the indicies to draw the faces this.indices = []; this.textureStride = options.enableWTextureCoord ? 3 : 2; /* The OBJ file format does a sort of compression when saving a model in a program like Blender. There are at least 3 sections (4 including textures) within the file. Each line in a section begins with the same string: * 'v': indicates vertex section * 'vn': indicates vertex normal section * 'f': indicates the faces section * 'vt': indicates vertex texture section (if textures were used on the model) Each of the above sections (except for the faces section) is a list/set of unique vertices. Each line of the faces section contains a list of (vertex, [texture], normal) groups. **Note:** The following documentation will use a capital "V" Vertex to denote the above (vertex, [texture], normal) groups whereas a lowercase "v" vertex is used to denote an X, Y, Z coordinate. Some examples: // the texture index is optional, both formats are possible for models // without a texture applied f 1/25 18/46 12/31 f 1//25 18//46 12//31 // A 3 vertex face with texture indices f 16/92/11 14/101/22 1/69/1 // A 4 vertex face f 16/92/11 40/109/40 38/114/38 14/101/22 The first two lines are examples of a 3 vertex face without a texture applied. The second is an example of a 3 vertex face with a texture applied. The third is an example of a 4 vertex face. Note: a face can contain N number of vertices. Each number that appears in one of the groups is a 1-based index corresponding to an item from the other sections (meaning that indexing starts at one and *not* zero). For example: `f 16/92/11` is saying to - take the 16th element from the [v] vertex array - take the 92nd element from the [vt] texture array - take the 11th element from the [vn] normal array and together they make a unique vertex. Using all 3+ unique Vertices from the face line will produce a polygon. Now, you could just go through the OBJ file and create a new vertex for each face line and WebGL will draw what appears to be the same model. However, vertices will be overlapped and duplicated all over the place. Consider a cube in 3D space centered about the origin and each side is 2 units long. The front face (with the positive Z-axis pointing towards you) would have a Top Right vertex (looking orthogonal to its normal) mapped at (1,1,1) The right face would have a Top Left vertex (looking orthogonal to its normal) at (1,1,1) and the top face would have a Bottom Right vertex (looking orthogonal to its normal) at (1,1,1). Each face has a vertex at the same coordinates, however, three distinct vertices will be drawn at the same spot. To solve the issue of duplicate Vertices (the `(vertex, [texture], normal)` groups), while iterating through the face lines, when a group is encountered the whole group string ('16/92/11') is checked to see if it exists in the packed.hashindices object, and if it doesn't, the indices it specifies are used to look up each attribute in the corresponding attribute arrays already created. The values are then copied to the corresponding unpacked array (flattened to play nice with WebGL's ELEMENT_ARRAY_BUFFER indexing), the group string is added to the hashindices set and the current unpacked index is used as this hashindices value so that the group of elements can be reused. The unpacked index is incremented. If the group string already exists in the hashindices object, its corresponding value is the index of that group and is appended to the unpacked indices array. */ const verts = []; const vertNormals = []; const textures = []; const materialNamesByIndex = []; const materialIndicesByName: MaterialNameToIndex = {}; // keep track of what material we've seen last let currentMaterialIndex = -1; let currentObjectByMaterialIndex = 0; // unpacking stuff const unpacked: UnpackedAttrs = { verts: [], norms: [], textures: [], hashindices: {}, indices: [[]], materialIndices: [], index: 0, }; const VERTEX_RE = /^v\s/; const NORMAL_RE = /^vn\s/; const TEXTURE_RE = /^vt\s/; const FACE_RE = /^f\s/; const WHITESPACE_RE = /\s+/; const USE_MATERIAL_RE = /^usemtl/; // array of lines separated by the newline const lines = objectData.split("\n"); for (let line of lines) { line = line.trim(); if (!line || line.startsWith("#")) { continue; } const elements = line.split(WHITESPACE_RE); elements.shift(); if (VERTEX_RE.test(line)) { // if this is a vertex verts.push(...elements); } else if (NORMAL_RE.test(line)) { // if this is a vertex normal vertNormals.push(...elements); } else if (TEXTURE_RE.test(line)) { let coords = elements; // by default, the loader will only look at the U and V // coordinates of the vt declaration. So, this truncates the // elements to only those 2 values. If W texture coordinate // support is enabled, then the texture coordinate is // expected to have three values in it. if (elements.length > 2 && !options.enableWTextureCoord) { coords = elements.slice(0, 2); } else if (elements.length === 2 && options.enableWTextureCoord) { // If for some reason W texture coordinate support is enabled // and only the U and V coordinates are given, then we supply // the default value of 0 so that the stride length is correct // when the textures are unpacked below. coords.push("0"); } textures.push(...coords); } else if (USE_MATERIAL_RE.test(line)) { const materialName = elements[0]; // check to see if we've ever seen it before if (!(materialName in materialIndicesByName)) { // new material we've never seen materialNamesByIndex.push(materialName); materialIndicesByName[materialName] = materialNamesByIndex.length - 1; // push new array into indices // already contains an array at index zero, don't add if (materialIndicesByName[materialName] > 0) { unpacked.indices.push([]); } } // keep track of the current material index currentMaterialIndex = materialIndicesByName[materialName]; // update current index array currentObjectByMaterialIndex = currentMaterialIndex; } else if (FACE_RE.test(line)) { // if this is a face /* split this face into an array of Vertex groups for example: f 16/92/11 14/101/22 1/69/1 becomes: ['16/92/11', '14/101/22', '1/69/1']; */ const triangles = triangulate(elements); for (const triangle of triangles) { for (let j = 0, eleLen = triangle.length; j < eleLen; j++) { const hash = triangle[j] + "," + currentMaterialIndex; if (hash in unpacked.hashindices) { unpacked.indices[currentObjectByMaterialIndex].push(unpacked.hashindices[hash]); } else { /* Each element of the face line array is a Vertex which has its attributes delimited by a forward slash. This will separate each attribute into another array: '19/92/11' becomes: Vertex = ['19', '92', '11']; where Vertex[0] is the vertex index Vertex[1] is the texture index Vertex[2] is the normal index Think of faces having Vertices which are comprised of the attributes location (v), texture (vt), and normal (vn). */ const vertex = triangle[j].split("/"); // it's possible for faces to only specify the vertex // and the normal. In this case, vertex will only have // a length of 2 and not 3 and the normal will be the // second item in the list with an index of 1. const normalIndex = vertex.length - 1; /* The verts, textures, and vertNormals arrays each contain a flattend array of coordinates. Because it gets confusing by referring to Vertex and then vertex (both are different in my descriptions) I will explain what's going on using the vertexNormals array: vertex[2] will contain the one-based index of the vertexNormals section (vn). One is subtracted from this index number to play nice with javascript's zero-based array indexing. Because vertexNormal is a flattened array of x, y, z values, simple pointer arithmetic is used to skip to the start of the vertexNormal, then the offset is added to get the correct component: +0 is x, +1 is y, +2 is z. This same process is repeated for verts and textures. */ // Vertex position unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 0]); unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 1]); unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 2]); // Vertex textures if (textures.length) { const stride = options.enableWTextureCoord ? 3 : 2; unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 0]); unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 1]); if (options.enableWTextureCoord) { unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 2]); } } // Vertex normals unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 0]); unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 1]); unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 2]); // Vertex material indices unpacked.materialIndices.push(currentMaterialIndex); // add the newly created Vertex to the list of indices unpacked.hashindices[hash] = unpacked.index; unpacked.indices[currentObjectByMaterialIndex].push(unpacked.hashindices[hash]); // increment the counter unpacked.index += 1; } } } } } this.vertices = unpacked.verts; this.vertexNormals = unpacked.norms; this.textures = unpacked.textures; this.vertexMaterialIndices = unpacked.materialIndices; this.indices = unpacked.indices[currentObjectByMaterialIndex]; this.indicesPerMaterial = unpacked.indices; this.materialNames = materialNamesByIndex; this.materialIndices = materialIndicesByName; this.materialsByIndex = {}; if (options.calcTangentsAndBitangents) { this.calculateTangentsAndBitangents(); } } /** * Calculates the tangents and bitangents of the mesh that forms an orthogonal basis together with the * normal in the direction of the texture coordinates. These are useful for setting up the TBN matrix * when distorting the normals through normal maps. * Method derived from: http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/ * * This method requires the normals and texture coordinates to be parsed and set up correctly. * Adds the tangents and bitangents as members of the class instance. */ calculateTangentsAndBitangents() { console.assert( !!( this.vertices && this.vertices.length && this.vertexNormals && this.vertexNormals.length && this.textures && this.textures.length ), "Missing attributes for calculating tangents and bitangents", ); const unpacked = { tangents: [...new Array(this.vertices.length)].map(_ => 0), bitangents: [...new Array(this.vertices.length)].map(_ => 0), }; // Loop through all faces in the whole mesh const indices = this.indices; const vertices = this.vertices; const normals = this.vertexNormals; const uvs = this.textures; for (let i = 0; i < indices.length; i += 3) { const i0 = indices[i + 0]; const i1 = indices[i + 1]; const i2 = indices[i + 2]; const x_v0 = vertices[i0 * 3 + 0]; const y_v0 = vertices[i0 * 3 + 1]; const z_v0 = vertices[i0 * 3 + 2]; const x_uv0 = uvs[i0 * 2 + 0]; const y_uv0 = uvs[i0 * 2 + 1]; const x_v1 = vertices[i1 * 3 + 0]; const y_v1 = vertices[i1 * 3 + 1]; const z_v1 = vertices[i1 * 3 + 2]; const x_uv1 = uvs[i1 * 2 + 0]; const y_uv1 = uvs[i1 * 2 + 1]; const x_v2 = vertices[i2 * 3 + 0]; const y_v2 = vertices[i2 * 3 + 1]; const z_v2 = vertices[i2 * 3 + 2]; const x_uv2 = uvs[i2 * 2 + 0]; const y_uv2 = uvs[i2 * 2 + 1]; const x_deltaPos1 = x_v1 - x_v0; const y_deltaPos1 = y_v1 - y_v0; const z_deltaPos1 = z_v1 - z_v0; const x_deltaPos2 = x_v2 - x_v0; const y_deltaPos2 = y_v2 - y_v0; const z_deltaPos2 = z_v2 - z_v0; const x_uvDeltaPos1 = x_uv1 - x_uv0; const y_uvDeltaPos1 = y_uv1 - y_uv0; const x_uvDeltaPos2 = x_uv2 - x_uv0; const y_uvDeltaPos2 = y_uv2 - y_uv0; const rInv = x_uvDeltaPos1 * y_uvDeltaPos2 - y_uvDeltaPos1 * x_uvDeltaPos2; const r = 1.0 / Math.abs(rInv < 0.0001 ? 1.0 : rInv); // Tangent const x_tangent = (x_deltaPos1 * y_uvDeltaPos2 - x_deltaPos2 * y_uvDeltaPos1) * r; const y_tangent = (y_deltaPos1 * y_uvDeltaPos2 - y_deltaPos2 * y_uvDeltaPos1) * r; const z_tangent = (z_deltaPos1 * y_uvDeltaPos2 - z_deltaPos2 * y_uvDeltaPos1) * r; // Bitangent const x_bitangent = (x_deltaPos2 * x_uvDeltaPos1 - x_deltaPos1 * x_uvDeltaPos2) * r; const y_bitangent = (y_deltaPos2 * x_uvDeltaPos1 - y_deltaPos1 * x_uvDeltaPos2) * r; const z_bitangent = (z_deltaPos2 * x_uvDeltaPos1 - z_deltaPos1 * x_uvDeltaPos2) * r; // Gram-Schmidt orthogonalize //t = glm::normalize(t - n * glm:: dot(n, t)); const x_n0 = normals[i0 * 3 + 0]; const y_n0 = normals[i0 * 3 + 1]; const z_n0 = normals[i0 * 3 + 2]; const x_n1 = normals[i1 * 3 + 0]; const y_n1 = normals[i1 * 3 + 1]; const z_n1 = normals[i1 * 3 + 2]; const x_n2 = normals[i2 * 3 + 0]; const y_n2 = normals[i2 * 3 + 1]; const z_n2 = normals[i2 * 3 + 2]; // Tangent const n0_dot_t = x_tangent * x_n0 + y_tangent * y_n0 + z_tangent * z_n0; const n1_dot_t = x_tangent * x_n1 + y_tangent * y_n1 + z_tangent * z_n1; const n2_dot_t = x_tangent * x_n2 + y_tangent * y_n2 + z_tangent * z_n2; const x_resTangent0 = x_tangent - x_n0 * n0_dot_t; const y_resTangent0 = y_tangent - y_n0 * n0_dot_t; const z_resTangent0 = z_tangent - z_n0 * n0_dot_t; const x_resTangent1 = x_tangent - x_n1 * n1_dot_t; const y_resTangent1 = y_tangent - y_n1 * n1_dot_t; const z_resTangent1 = z_tangent - z_n1 * n1_dot_t; const x_resTangent2 = x_tangent - x_n2 * n2_dot_t; const y_resTangent2 = y_tangent - y_n2 * n2_dot_t; const z_resTangent2 = z_tangent - z_n2 * n2_dot_t; const magTangent0 = Math.sqrt( x_resTangent0 * x_resTangent0 + y_resTangent0 * y_resTangent0 + z_resTangent0 * z_resTangent0, ); const magTangent1 = Math.sqrt( x_resTangent1 * x_resTangent1 + y_resTangent1 * y_resTangent1 + z_resTangent1 * z_resTangent1, ); const magTangent2 = Math.sqrt( x_resTangent2 * x_resTangent2 + y_resTangent2 * y_resTangent2 + z_resTangent2 * z_resTangent2, ); // Bitangent const n0_dot_bt = x_bitangent * x_n0 + y_bitangent * y_n0 + z_bitangent * z_n0; const n1_dot_bt = x_bitangent * x_n1 + y_bitangent * y_n1 + z_bitangent * z_n1; const n2_dot_bt = x_bitangent * x_n2 + y_bitangent * y_n2 + z_bitangent * z_n2; const x_resBitangent0 = x_bitangent - x_n0 * n0_dot_bt; const y_resBitangent0 = y_bitangent - y_n0 * n0_dot_bt; const z_resBitangent0 = z_bitangent - z_n0 * n0_dot_bt; const x_resBitangent1 = x_bitangent - x_n1 * n1_dot_bt; const y_resBitangent1 = y_bitangent - y_n1 * n1_dot_bt; const z_resBitangent1 = z_bitangent - z_n1 * n1_dot_bt; const x_resBitangent2 = x_bitangent - x_n2 * n2_dot_bt; const y_resBitangent2 = y_bitangent - y_n2 * n2_dot_bt; const z_resBitangent2 = z_bitangent - z_n2 * n2_dot_bt; const magBitangent0 = Math.sqrt( x_resBitangent0 * x_resBitangent0 + y_resBitangent0 * y_resBitangent0 + z_resBitangent0 * z_resBitangent0, ); const magBitangent1 = Math.sqrt( x_resBitangent1 * x_resBitangent1 + y_resBitangent1 * y_resBitangent1 + z_resBitangent1 * z_resBitangent1, ); const magBitangent2 = Math.sqrt( x_resBitangent2 * x_resBitangent2 + y_resBitangent2 * y_resBitangent2 + z_resBitangent2 * z_resBitangent2, ); unpacked.tangents[i0 * 3 + 0] += x_resTangent0 / magTangent0; unpacked.tangents[i0 * 3 + 1] += y_resTangent0 / magTangent0; unpacked.tangents[i0 * 3 + 2] += z_resTangent0 / magTangent0; unpacked.tangents[i1 * 3 + 0] += x_resTangent1 / magTangent1; unpacked.tangents[i1 * 3 + 1] += y_resTangent1 / magTangent1; unpacked.tangents[i1 * 3 + 2] += z_resTangent1 / magTangent1; unpacked.tangents[i2 * 3 + 0] += x_resTangent2 / magTangent2; unpacked.tangents[i2 * 3 + 1] += y_resTangent2 / magTangent2; unpacked.tangents[i2 * 3 + 2] += z_resTangent2 / magTangent2; unpacked.bitangents[i0 * 3 + 0] += x_resBitangent0 / magBitangent0; unpacked.bitangents[i0 * 3 + 1] += y_resBitangent0 / magBitangent0; unpacked.bitangents[i0 * 3 + 2] += z_resBitangent0 / magBitangent0; unpacked.bitangents[i1 * 3 + 0] += x_resBitangent1 / magBitangent1; unpacked.bitangents[i1 * 3 + 1] += y_resBitangent1 / magBitangent1; unpacked.bitangents[i1 * 3 + 2] += z_resBitangent1 / magBitangent1; unpacked.bitangents[i2 * 3 + 0] += x_resBitangent2 / magBitangent2; unpacked.bitangents[i2 * 3 + 1] += y_resBitangent2 / magBitangent2; unpacked.bitangents[i2 * 3 + 2] += z_resBitangent2 / magBitangent2; // TODO: check handedness } this.tangents = unpacked.tangents; this.bitangents = unpacked.bitangents; } /** * @param layout - A {@link Layout} object that describes the * desired memory layout of the generated buffer * @return The packed array in the ... TODO */ makeBufferData(layout: Layout): ArrayBufferWithItemSize { const numItems = this.vertices.length / 3; const buffer: ArrayBufferWithItemSize = new ArrayBuffer(layout.stride * numItems); buffer.numItems = numItems; const dataView = new DataView(buffer); for (let i = 0, vertexOffset = 0; i < numItems; i++) { vertexOffset = i * layout.stride; // copy in the vertex data in the order and format given by the // layout param for (const attribute of layout.attributes) { const offset = vertexOffset + layout.attributeMap[attribute.key].offset; switch (attribute.key) { case Layout.POSITION.key: dataView.setFloat32(offset, this.vertices[i * 3], true); dataView.setFloat32(offset + 4, this.vertices[i * 3 + 1], true); dataView.setFloat32(offset + 8, this.vertices[i * 3 + 2], true); break; case Layout.UV.key: dataView.setFloat32(offset, this.textures[i * 2], true); dataView.setFloat32(offset + 4, this.textures[i * 2 + 1], true); break; case Layout.NORMAL.key: dataView.setFloat32(offset, this.vertexNormals[i * 3], true); dataView.setFloat32(offset + 4, this.vertexNormals[i * 3 + 1], true); dataView.setFloat32(offset + 8, this.vertexNormals[i * 3 + 2], true); break; case Layout.MATERIAL_INDEX.key: dataView.setInt16(offset, this.vertexMaterialIndices[i], true); break; case Layout.AMBIENT.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.ambient[0], true); dataView.setFloat32(offset + 4, material.ambient[1], true); dataView.setFloat32(offset + 8, material.ambient[2], true); break; } case Layout.DIFFUSE.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.diffuse[0], true); dataView.setFloat32(offset + 4, material.diffuse[1], true); dataView.setFloat32(offset + 8, material.diffuse[2], true); break; } case Layout.SPECULAR.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.specular[0], true); dataView.setFloat32(offset + 4, material.specular[1], true); dataView.setFloat32(offset + 8, material.specular[2], true); break; } case Layout.SPECULAR_EXPONENT.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.specularExponent, true); break; } case Layout.EMISSIVE.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.emissive[0], true); dataView.setFloat32(offset + 4, material.emissive[1], true); dataView.setFloat32(offset + 8, material.emissive[2], true); break; } case Layout.TRANSMISSION_FILTER.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.transmissionFilter[0], true); dataView.setFloat32(offset + 4, material.transmissionFilter[1], true); dataView.setFloat32(offset + 8, material.transmissionFilter[2], true); break; } case Layout.DISSOLVE.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.dissolve, true); break; } case Layout.ILLUMINATION.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setInt16(offset, material.illumination, true); break; } case Layout.REFRACTION_INDEX.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.refractionIndex, true); break; } case Layout.SHARPNESS.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setFloat32(offset, material.sharpness, true); break; } case Layout.ANTI_ALIASING.key: { const materialIndex = this.vertexMaterialIndices[i]; const material = this.materialsByIndex[materialIndex]; if (!material) { console.warn( 'Material "' + this.materialNames[materialIndex] + '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', ); break; } dataView.setInt16(offset, material.antiAliasing ? 1 : 0, true); break; } } } } return buffer; } makeIndexBufferData(): Uint16ArrayWithItemSize { const buffer: Uint16ArrayWithItemSize = new Uint16Array(this.indices); buffer.numItems = this.indices.length; return buffer; } makeIndexBufferDataForMaterials(...materialIndices: Array<number>): Uint16ArrayWithItemSize { const indices: number[] = new Array<number>().concat( ...materialIndices.map(mtlIdx => this.indicesPerMaterial[mtlIdx]), ); const buffer: Uint16ArrayWithItemSize = new Uint16Array(indices); buffer.numItems = indices.length; return buffer; } addMaterialLibrary(mtl: MaterialLibrary) { for (const name in mtl.materials) { if (!(name in this.materialIndices)) { // This material is not referenced by the mesh continue; } const material = mtl.materials[name]; // Find the material index for this material const materialIndex = this.materialIndices[material.name]; // Put the material into the materialsByIndex object at the right // spot as determined when the obj file was parsed this.materialsByIndex[materialIndex] = material; } } } function* triangulate(elements: string[]) { if (elements.length <= 3) { yield elements; } else if (elements.length === 4) { yield [elements[0], elements[1], elements[2]]; yield [elements[2], elements[3], elements[0]]; } else { for (let i = 1; i < elements.length - 1; i++) { yield [elements[0], elements[i], elements[i + 1]]; } } }