UNPKG

vim-webgl-component

Version:

A demonstration app built on top of the vim-webgl-viewer

599 lines (598 loc) 25.8 kB
"use strict"; /** * @module vim-ts */ Object.defineProperty(exports, "__esModule", { value: true }); exports.G3d = exports.VimAttributes = void 0; const bfast_1 = require("../bfast"); /** * See https://github.com/vimaec/vim#vim-geometry-attributes */ class VimAttributes { } exports.VimAttributes = VimAttributes; VimAttributes.positions = 'g3d:vertex:position:0:float32:3'; VimAttributes.indices = 'g3d:corner:index:0:int32:1'; VimAttributes.instanceMeshes = 'g3d:instance:mesh:0:int32:1'; VimAttributes.instanceTransforms = 'g3d:instance:transform:0:float32:16'; VimAttributes.instanceNodes = 'g3d:instance:element:0:int32:1'; VimAttributes.instanceFlags = 'g3d:instance:flags:0:uint16:1'; VimAttributes.meshSubmeshes = 'g3d:mesh:submeshoffset:0:int32:1'; VimAttributes.submeshIndexOffsets = 'g3d:submesh:indexoffset:0:int32:1'; VimAttributes.submeshMaterials = 'g3d:submesh:material:0:int32:1'; VimAttributes.materialColors = 'g3d:material:color:0:float32:4'; VimAttributes.all = [ VimAttributes.positions, VimAttributes.indices, VimAttributes.instanceMeshes, VimAttributes.instanceTransforms, VimAttributes.instanceFlags, VimAttributes.meshSubmeshes, VimAttributes.submeshIndexOffsets, VimAttributes.submeshMaterials, VimAttributes.materialColors ]; /** * G3D is a simple, efficient, generic binary format for storing and transmitting geometry. * The G3D format is designed to be used either as a serialization format or as an in-memory data structure. * A G3d with specific attributes according to the VIM format specification. * See https://github.com/vimaec/vim#vim-geometry-attributes for the vim specification. * See https://github.com/vimaec/g3d for the g3d specification. */ class G3d { constructor(instanceMeshes, instanceFlags, instanceTransforms, instanceNodes, meshSubmeshes, submeshIndexOffsets, submeshMaterials, indices, positions, materialColors) { /** * Opaque white */ this.DEFAULT_COLOR = new Float32Array([1, 1, 1, 1]); this.getVertexCount = () => this.positions.length / G3d.POSITION_SIZE; this.getIndexCount = () => this.indices.length; // ------------- Meshes ----------------- this.getMeshCount = () => this.meshSubmeshes.length; // ------------- Instances ----------------- this.getInstanceCount = () => this.instanceMeshes.length; // ------------- Material ----------------- this.getMaterialCount = () => this.materialColors.length / G3d.COLOR_SIZE; this.instanceMeshes = instanceMeshes; this.instanceFlags = instanceFlags; this.instanceTransforms = instanceTransforms; this.instanceNodes = instanceNodes; this.meshSubmeshes = meshSubmeshes; this.submeshIndexOffset = submeshIndexOffsets; this.submeshMaterial = submeshMaterials; this.indices = indices instanceof Uint32Array ? indices : new Uint32Array(indices.buffer); this.positions = positions; this.materialColors = materialColors; if (this.instanceFlags === undefined) { this.instanceFlags = new Uint16Array(this.instanceMeshes.length); } if (this.instanceNodes === undefined) { this.instanceNodes = new Int32Array(instanceMeshes.length); for (let i = 0; i < this.instanceNodes.length; i++) { this.instanceNodes[i] = i; } } this.meshVertexOffsets = this.computeMeshVertexOffsets(); this.rebaseIndices(); this.meshInstances = this.computeMeshInstances(); this.meshOpaqueCount = this.computeMeshOpaqueCount(); this.sortSubmeshes(); const range = this.computeSubmeshVertexRange(); this.submeshVertexStart = range.start; this.submeshVertexEnd = range.end; } computeSubmeshVertexRange() { const submeshCount = this.getSubmeshCount(); const start = new Int32Array(submeshCount); const end = new Int32Array(submeshCount); for (let sub = 0; sub < submeshCount; sub++) { let min = Number.MAX_SAFE_INTEGER; let max = Number.MIN_SAFE_INTEGER; const subStart = this.getSubmeshIndexStart(sub); const subEnd = this.getSubmeshIndexEnd(sub); for (let i = subStart; i < subEnd; i++) { const index = this.indices[i]; min = Math.min(min, index); max = Math.max(min, index); } start[sub] = min; end[sub] = max; } return { start, end }; } static async createFromPath(path) { const f = await fetch(path); const buffer = await f.arrayBuffer(); const bfast = new bfast_1.BFast({ buffer }); return this.createFromBfast(bfast); } static async createFromBfast(bfast) { const values = await Promise.all([ bfast.getInt32Array(VimAttributes.instanceMeshes), bfast.getUint16Array(VimAttributes.instanceFlags), bfast.getFloat32Array(VimAttributes.instanceTransforms), bfast.getInt32Array(VimAttributes.instanceNodes), bfast.getInt32Array(VimAttributes.meshSubmeshes), bfast.getInt32Array(VimAttributes.submeshIndexOffsets), bfast.getInt32Array(VimAttributes.submeshMaterials), bfast.getInt32Array(VimAttributes.indices), bfast.getFloat32Array(VimAttributes.positions), bfast.getFloat32Array(VimAttributes.materialColors) ]); return new G3d(...values); } /** * Computes the index of the first vertex of each mesh */ computeMeshVertexOffsets() { const result = new Int32Array(this.getMeshCount()); for (let m = 0; m < result.length; m++) { let min = Number.MAX_SAFE_INTEGER; const start = this.getMeshIndexStart(m, 'all'); const end = this.getMeshIndexEnd(m, 'all'); for (let i = start; i < end; i++) { min = Math.min(min, this.indices[i]); } result[m] = min; } return result; } /** * Computes all instances pointing to each mesh. */ computeMeshInstances() { const result = new Array(this.getMeshCount()); for (let i = 0; i < this.instanceMeshes.length; i++) { const mesh = this.instanceMeshes[i]; if (mesh < 0) continue; const instanceIndices = result[mesh]; if (instanceIndices) instanceIndices.push(i); else result[mesh] = [i]; } return result; } /** * Reorders submeshIndexOffset, submeshMaterials and indices * such that for each mesh, submeshes are sorted according to material alpha. * This enables efficient splitting of arrays into opaque and transparent continuous ranges. */ sortSubmeshes() { // We need to compute where submeshes end before we can reorder them. const submeshEnd = this.computeSubmeshEnd(); // We need to compute mesh index offsets from before we swap thins around to recompute new submesh offsets. const meshIndexOffsets = this.computeMeshIndexOffsets(); const meshCount = this.getMeshCount(); const meshReordered = new Array(meshCount); const submeshArrays = [ this.submeshIndexOffset, this.submeshMaterial, submeshEnd ]; // Largest mesh size thus minimum buffer size to use to reorder indices. const largestMesh = this.reorderSubmeshes(submeshArrays, meshReordered); this.reorderIndices(meshIndexOffsets, submeshEnd, meshReordered, largestMesh); } /** * Stores result of getSubmeshIndexEnd for each submesh in an array */ computeSubmeshEnd() { const submeshCount = this.getSubmeshCount(); const result = new Int32Array(submeshCount); for (let s = 0; s < submeshCount; s++) { result[s] = this.getSubmeshIndexEnd(s); } return result; } /** * Stores result of getMeshIndexStart for each mesh in an array */ computeMeshIndexOffsets() { const meshCount = this.getMeshCount(); const result = new Int32Array(meshCount); for (let m = 0; m < meshCount; m++) { result[m] = this.getMeshIndexStart(m, 'all'); } return result; } /** * Reorder submesh arrays and returns size of largest reordered mesh */ reorderSubmeshes(submeshArrays, reordered) { const meshCount = this.getMeshCount(); let largestMesh = 0; for (let m = 0; m < meshCount; m++) { const subStart = this.getMeshSubmeshStart(m, 'all'); const subEnd = this.getMeshSubmeshEnd(m, 'all'); if (subEnd - subStart <= 1) { continue; } largestMesh = Math.max(largestMesh, this.getMeshIndexCount(m, 'all')); reordered[m] = this.Sort(subStart, subEnd, (i) => this.getSubmeshAlpha(i), submeshArrays); } return largestMesh; } /** * Sorts the range from start to end in every array provided in arrays in increasing criterion order. * Using a simple bubble sort, there is a limited number of submeshes per mesh. */ Sort(start, end, criterion, arrays) { let swapped = false; while (true) { let loop = false; for (let i = start; i < end - 1; i++) { if (criterion(i) < criterion(i + 1)) { loop = true; swapped = true; for (let j = 0; j < arrays.length; j++) { const array = arrays[j]; const t = array[i]; array[i] = array[i + 1]; array[i + 1] = t; } } } if (!loop) { break; } } return swapped; } /** * Reorders the index buffer to match the new order of the submesh arrays. */ reorderIndices(meshIndexOffsets, submeshEnd, meshReordered, bufferSize) { const meshCount = this.getMeshCount(); const buffer = new Float32Array(bufferSize); for (let m = 0; m < meshCount; m++) { if (!meshReordered[m]) continue; const meshOffset = meshIndexOffsets[m]; const subStart = this.getMeshSubmeshStart(m, 'all'); const subEnd = this.getMeshSubmeshEnd(m, 'all'); let index = 0; // Copy indices -> buffer, in sorted order. for (let s = subStart; s < subEnd; s++) { const start = this.submeshIndexOffset[s]; const end = submeshEnd[s]; // Change submesh offset to match new ordering this.submeshIndexOffset[s] = meshOffset + index; for (let i = start; i < end; i++) { buffer[index++] = this.indices[i]; } } // Copy buffer -> indices for (let i = 0; i < index; i++) { this.indices[meshOffset + i] = buffer[i]; } } } /** * Rebase indices to be relative to its own mesh instead of to the whole g3d */ rebaseIndices() { const count = this.getMeshCount(); for (let m = 0; m < count; m++) { const offset = this.meshVertexOffsets[m]; const start = this.getMeshIndexStart(m, 'all'); const end = this.getMeshIndexEnd(m, 'all'); for (let i = start; i < end; i++) { this.indices[i] -= offset; } } } unbaseIndices() { const count = this.getMeshCount(); for (let m = 0; m < count; m++) { const offset = this.meshVertexOffsets[m]; const start = this.getMeshIndexStart(m, 'all'); const end = this.getMeshIndexEnd(m, 'all'); for (let i = start; i < end; i++) { this.indices[i] += offset; } } } /** * Computes an array where true if any of the materials used by a mesh has transparency. */ computeMeshOpaqueCount() { const result = new Int32Array(this.getMeshCount()); for (let m = 0; m < result.length; m++) { const subStart = this.getMeshSubmeshStart(m, 'all'); const subEnd = this.getMeshSubmeshEnd(m, 'all'); for (let s = subStart; s < subEnd; s++) { const alpha = this.getSubmeshAlpha(s); result[m] += alpha === 1 ? 1 : 0; } } return result; } // ------------- All ----------------- /**Given VIM instance indices returns the corresponding G3d indices */ remapInstances(instances) { const map = new Map(); for (let i = 0; i < instances.length; i++) { map.set(this.instanceNodes[i], i); } return instances.map((i) => map.get(i)); } getMeshInstanceCount(mesh) { return this.meshInstances[mesh]?.length ?? 0; } getMeshIndexStart(mesh, section = 'all') { const sub = this.getMeshSubmeshStart(mesh, section); return this.getSubmeshIndexStart(sub); } getMeshIndexEnd(mesh, section = 'all') { const sub = this.getMeshSubmeshEnd(mesh, section); return this.getSubmeshIndexEnd(sub - 1); } getMeshIndexCount(mesh, section = 'all') { return (this.getMeshIndexEnd(mesh, section) - this.getMeshIndexStart(mesh, section)); } getMeshVertexStart(mesh) { return this.meshVertexOffsets[mesh]; } getMeshVertexEnd(mesh) { return mesh < this.meshVertexOffsets.length - 1 ? this.meshVertexOffsets[mesh + 1] : this.getVertexCount(); } getMeshVertexCount(mesh) { return this.getMeshVertexEnd(mesh) - this.getMeshVertexStart(mesh); } getMeshSubmeshStart(mesh, section = 'all') { if (section === 'transparent') { return this.getMeshSubmeshEnd(mesh, 'opaque'); } return this.meshSubmeshes[mesh]; } getMeshSubmeshEnd(mesh, section = 'all') { if (section === 'opaque') { return this.meshSubmeshes[mesh] + this.meshOpaqueCount[mesh]; } return mesh < this.meshSubmeshes.length - 1 ? this.meshSubmeshes[mesh + 1] : this.getSubmeshCount(); } getMeshSubmeshCount(mesh, section = 'all') { const end = this.getMeshSubmeshEnd(mesh, section); const start = this.getMeshSubmeshStart(mesh, section); return end - start; } getMeshHasTransparency(mesh) { return this.getMeshSubmeshCount(mesh, 'transparent') > 0; } // ------------- Submeshes ----------------- getSubmeshIndexStart(submesh) { return submesh < this.submeshIndexOffset.length ? this.submeshIndexOffset[submesh] : this.indices.length; } getSubmeshIndexEnd(submesh) { return submesh < this.submeshIndexOffset.length - 1 ? this.submeshIndexOffset[submesh + 1] : this.indices.length; } getSubmeshIndexCount(submesh) { return this.getSubmeshIndexEnd(submesh) - this.getSubmeshIndexStart(submesh); } getSubmeshVertexStart(submesh) { return this.submeshVertexStart[submesh]; } getSubmeshVertexEnd(submesh) { return this.submeshVertexEnd[submesh]; } getSubmeshVertexCount(submesh) { return this.getSubmeshVertexEnd(submesh) - this.getSubmeshVertexStart(submesh); } /** * Returns color of given submesh as a 4-number array (RGBA) * @param submesh g3d submesh index */ getSubmeshColor(submesh) { return this.getMaterialColor(this.submeshMaterial[submesh]); } /** * Returns color of given submesh as a 4-number array (RGBA) * @param submesh g3d submesh index */ getSubmeshAlpha(submesh) { return this.getMaterialAlpha(this.submeshMaterial[submesh]); } /** * Returns true if submesh is transparent. * @param submesh g3d submesh index */ getSubmeshIsTransparent(submesh) { return this.getSubmeshAlpha(submesh) < 1; } /** * Returns the total number of mesh in the g3d */ getSubmeshCount() { return this.submeshIndexOffset.length; } /** * Returns true if instance has given flag enabled. * @param instance instance to check. * @param flag to check against. */ getInstanceHasFlag(instance, flag) { return (this.instanceFlags[instance] & flag) > 0; } /** * Returns mesh index of given instance * @param instance g3d instance index */ getInstanceMesh(instance) { return this.instanceMeshes[instance]; } /** * Returns an 16 number array representation of the matrix for given instance * @param instance g3d instance index */ getInstanceMatrix(instance) { return this.instanceTransforms.subarray(instance * G3d.MATRIX_SIZE, (instance + 1) * G3d.MATRIX_SIZE); } /** * Returns color of given material as a 4-number array (RGBA) * @param material g3d material index */ getMaterialColor(material) { if (material < 0) return this.DEFAULT_COLOR; return this.materialColors.subarray(material * G3d.COLOR_SIZE, (material + 1) * G3d.COLOR_SIZE); } /** * Returns the alpha component of given material * @param material */ getMaterialAlpha(material) { if (material < 0) return 1; const index = material * G3d.COLOR_SIZE + G3d.COLOR_SIZE - 1; const result = this.materialColors[index]; return result; } /** * Concatenates two g3ds into a new g3d. * @deprecated */ append(other) { const _instanceFlags = new Uint16Array(this.instanceFlags.length + other.instanceFlags.length); _instanceFlags.set(this.instanceFlags); _instanceFlags.set(other.instanceFlags, this.instanceFlags.length); const _instanceMeshes = new Int32Array(this.instanceMeshes.length + other.instanceMeshes.length); _instanceMeshes.set(this.instanceMeshes); _instanceMeshes.set(other.instanceMeshes.map(m => m >= 0 ? (m + this.meshSubmeshes.length) : -1), this.instanceMeshes.length); const _instanceTransforms = new Float32Array(this.instanceTransforms.length + other.instanceTransforms.length); _instanceTransforms.set(this.instanceTransforms); _instanceTransforms.set(other.instanceTransforms, this.instanceTransforms.length); const _positions = new Float32Array(this.positions.length + other.positions.length); _positions.set(this.positions); _positions.set(other.positions, this.positions.length); this.unbaseIndices(); other.unbaseIndices(); const _indices = new Uint32Array(this.indices.length + other.indices.length); _indices.set(this.indices); _indices.set(other.indices.map(i => i + this.positions.length / 3), this.indices.length); this.rebaseIndices(); other.rebaseIndices(); const _meshSubmeshes = new Int32Array(this.meshSubmeshes.length + other.meshSubmeshes.length); _meshSubmeshes.set(this.meshSubmeshes); _meshSubmeshes.set(other.meshSubmeshes.map(s => s + this.submeshIndexOffset.length), this.meshSubmeshes.length); const _submeshIndexOffsets = new Int32Array(this.submeshIndexOffset.length + other.submeshIndexOffset.length); _submeshIndexOffsets.set(this.submeshIndexOffset); _submeshIndexOffsets.set(other.submeshIndexOffset.map(s => s + this.indices.length), this.submeshIndexOffset.length); const _submeshMaterials = new Int32Array(this.submeshMaterial.length + other.submeshMaterial.length); _submeshMaterials.set(this.submeshMaterial); _submeshMaterials.set(other.submeshMaterial.map(s => s >= 0 ? (s + this.materialColors.length / 4) : -1), this.submeshMaterial.length); const _materialColors = new Float32Array(this.materialColors.length + other.materialColors.length); _materialColors.set(this.materialColors); _materialColors.set(other.materialColors, this.materialColors.length); const g3d = new G3d(_instanceMeshes, _instanceFlags, _instanceTransforms, undefined, _meshSubmeshes, _submeshIndexOffsets, _submeshMaterials, _indices, _positions, _materialColors); return g3d; } validate() { const isPresent = (attribute, label) => { if (!attribute) { throw new Error(`Missing Attribute Buffer: ${label}`); } }; isPresent(this.positions, 'position'); isPresent(this.indices, 'indices'); isPresent(this.instanceMeshes, 'instanceMeshes'); isPresent(this.instanceTransforms, 'instanceTransforms'); isPresent(this.meshSubmeshes, 'meshSubmeshes'); isPresent(this.submeshIndexOffset, 'submeshIndexOffset'); isPresent(this.submeshMaterial, 'submeshMaterial'); isPresent(this.materialColors, 'materialColors'); // Basic if (this.positions.length % G3d.POSITION_SIZE !== 0) { throw new Error('Invalid position buffer, must be divisible by ' + G3d.POSITION_SIZE); } if (this.indices.length % 3 !== 0) { throw new Error('Invalid Index Count, must be divisible by 3'); } for (let i = 0; i < this.indices.length; i++) { if (this.indices[i] < 0 || this.indices[i] >= this.positions.length) { throw new Error('Vertex index out of bound'); } } // Instances if (this.instanceMeshes.length !== this.instanceTransforms.length / G3d.MATRIX_SIZE) { throw new Error('Instance buffers mismatched'); } if (this.instanceTransforms.length % G3d.MATRIX_SIZE !== 0) { throw new Error('Invalid InstanceTransform buffer, must respect arity ' + G3d.MATRIX_SIZE); } for (let i = 0; i < this.instanceMeshes.length; i++) { if (this.instanceMeshes[i] >= this.meshSubmeshes.length) { throw new Error('Instance Mesh Out of range.'); } } // Meshes for (let i = 0; i < this.meshSubmeshes.length; i++) { if (this.meshSubmeshes[i] < 0 || this.meshSubmeshes[i] >= this.submeshIndexOffset.length) { throw new Error('MeshSubmeshOffset out of bound at'); } } for (let i = 0; i < this.meshSubmeshes.length - 1; i++) { if (this.meshSubmeshes[i] >= this.meshSubmeshes[i + 1]) { throw new Error('MeshSubmesh out of sequence.'); } } // Submeshes if (this.submeshIndexOffset.length !== this.submeshMaterial.length) { throw new Error('Mismatched submesh buffers'); } for (let i = 0; i < this.submeshIndexOffset.length; i++) { if (this.submeshIndexOffset[i] < 0 || this.submeshIndexOffset[i] >= this.indices.length) { throw new Error('SubmeshIndexOffset out of bound'); } } for (let i = 0; i < this.submeshIndexOffset.length; i++) { if (this.submeshIndexOffset[i] % 3 !== 0) { throw new Error('Invalid SubmeshIndexOffset, must be divisible by 3'); } } for (let i = 0; i < this.submeshIndexOffset.length - 1; i++) { if (this.submeshIndexOffset[i] >= this.submeshIndexOffset[i + 1]) { throw new Error('SubmeshIndexOffset out of sequence.'); } } for (let i = 0; i < this.submeshMaterial.length; i++) { if (this.submeshMaterial[i] >= this.materialColors.length) { throw new Error('submeshMaterial out of bound'); } } // Materials if (this.materialColors.length % G3d.COLOR_SIZE !== 0) { throw new Error('Invalid material color buffer, must be divisible by ' + G3d.COLOR_SIZE); } console.assert(this.meshInstances.length === this.getMeshCount()); console.assert(this.meshOpaqueCount.length === this.getMeshCount()); console.assert(this.meshSubmeshes.length === this.getMeshCount()); console.assert(this.meshVertexOffsets.length === this.getMeshCount()); for (let m = 0; m < this.getMeshCount(); m++) { console.assert(this.getMeshSubmeshCount(m, 'opaque') + this.getMeshSubmeshCount(m, 'transparent') === this.getMeshSubmeshCount(m, 'all')); console.assert(this.getMeshIndexCount(m, 'opaque') + this.getMeshIndexCount(m, 'transparent') === this.getMeshIndexCount(m, 'all')); } } } exports.G3d = G3d; G3d.MATRIX_SIZE = 16; G3d.COLOR_SIZE = 4; G3d.POSITION_SIZE = 3;