UNPKG

@xeokit/xeokit-sdk

Version:

3D BIM IFC Viewer SDK for AEC engineering applications. Open Source JavaScript Toolkit based on pure WebGL for top performance, real-world coordinates and full double precision

717 lines (654 loc) 25.1 kB
import {Geometry} from './Geometry.js'; import {RenderState} from '../webgl/RenderState.js'; import {ArrayBuf} from '../webgl/ArrayBuf.js'; import {math} from '../math/math.js'; import {stats} from '../stats.js'; import {buildEdgeIndices} from '../math/buildEdgeIndices.js'; import {geometryCompressionUtils} from '../math/geometryCompressionUtils.js'; const memoryStats = stats.memory; const tempAABB = math.AABB3(); /** * @desc A {@link Geometry} that keeps its geometry data in both browser and GPU memory. * * ReadableGeometry uses more memory than {@link VBOGeometry}, which only stores its geometry data in GPU memory. * * ## Usage * * Creating a {@link Mesh} with a ReadableGeometry that defines a single triangle, plus a {@link PhongMaterial} with diffuse {@link Texture}: * * [[Run this example](/examples/index.html#geometry_ReadableGeometry)] * * ````javascript * import {Viewer, Mesh, ReadableGeometry, PhongMaterial, Texture} from "xeokit-sdk.es.js"; * * const viewer = new Viewer({ * canvasId: "myCanvas" * }); * * const myMesh = new Mesh(viewer.scene, { * geometry: new ReadableGeometry(viewer.scene, { * primitive: "triangles", * positions: [0.0, 3, 0.0, -3, -3, 0.0, 3, -3, 0.0], * normals: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0], * uv: [0.0, 0.0, 0.5, 1.0, 1.0, 0.0], * indices: [0, 1, 2] * }), * material: new PhongMaterial(viewer.scene, { * diffuseMap: new Texture(viewer.scene, { * src: "textures/diffuse/uvGrid2.jpg" * }), * backfaces: true * }) * }); * * // Get geometry data from browser memory: * * const positions = myMesh.geometry.positions; // Flat arrays * const normals = myMesh.geometry.normals; * const uv = myMesh.geometry.uv; * const indices = myMesh.geometry.indices; * * ```` */ class ReadableGeometry extends Geometry { /** @private */ get type() { return "ReadableGeometry"; } /** * @private * @returns {Boolean} */ get isReadableGeometry() { return true; } /** * @class ReadableGeometry @module xeokit @submodule geometry @constructor @param {Component} owner Owner component. When destroyed, the owner will destroy this component as well. @param {*} [cfg] Configs @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted. @param {String:Object} [cfg.meta] Optional map of user-defined metadata to attach to this Geometry. @param [cfg.primitive="triangles"] {String} The primitive type. Accepted values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'. @param [cfg.positions] {Number[]} Positions array. @param [cfg.normals] {Number[]} Vertex normal vectors array. @param [cfg.uv] {Number[]} UVs array. @param [cfg.colors] {Number[]} Vertex colors. @param [cfg.indices] {Number[]} Indices array. @param [cfg.autoVertexNormals=false] {Boolean} Set true to automatically generate normal vectors from the positions and indices, if those are supplied. @param [cfg.compressGeometry=false] {Boolean} Stores positions, colors, normals and UVs in compressGeometry and oct-encoded formats for reduced memory footprint and GPU bus usage. @param [cfg.edgeThreshold=10] {Number} When a {@link Mesh} renders this Geometry as wireframe, this indicates the threshold angle (in degrees) between the face normals of adjacent triangles below which the edge is discarded. @extends Component * @param owner * @param cfg */ constructor(owner, cfg = {}) { super(owner, cfg); this._state = new RenderState({ // Arrays for emphasis effects are got from xeokit.Geometry friend methods compressGeometry: !!cfg.compressGeometry, primitive: null, // WebGL enum primitiveName: null, // String positions: null, // Uint16Array when compressGeometry == true, else Float32Array normals: null, // Uint8Array when compressGeometry == true, else Float32Array colors: null, uv: null, // Uint8Array when compressGeometry == true, else Float32Array indices: null, positionsDecodeMatrix: null, // Set when compressGeometry == true uvDecodeMatrix: null, // Set when compressGeometry == true positionsBuf: null, normalsBuf: null, colorsbuf: null, uvBuf: null, indicesBuf: null, hash: "" }); this._numTriangles = 0; this._edgeThreshold = cfg.edgeThreshold || 10.0; // Lazy-generated VBOs this._edgeIndicesBuf = null; this._pickTrianglePositionsBuf = null; this._pickTriangleColorsBuf = null; // Local-space Boundary3D this._aabbDirty = true; this._boundingSphere = true; this._aabb = null; this._aabbDirty = true; this._obb = null; this._obbDirty = true; const state = this._state; const gl = this.scene.canvas.gl; // Primitive type cfg.primitive = cfg.primitive || "triangles"; switch (cfg.primitive) { case "points": state.primitive = gl.POINTS; state.primitiveName = cfg.primitive; break; case "lines": state.primitive = gl.LINES; state.primitiveName = cfg.primitive; break; case "line-loop": state.primitive = gl.LINE_LOOP; state.primitiveName = cfg.primitive; break; case "line-strip": state.primitive = gl.LINE_STRIP; state.primitiveName = cfg.primitive; break; case "triangles": state.primitive = gl.TRIANGLES; state.primitiveName = cfg.primitive; break; case "triangle-strip": state.primitive = gl.TRIANGLE_STRIP; state.primitiveName = cfg.primitive; break; case "triangle-fan": state.primitive = gl.TRIANGLE_FAN; state.primitiveName = cfg.primitive; break; default: this.error("Unsupported value for 'primitive': '" + cfg.primitive + "' - supported values are 'points', 'lines', 'line-loop', 'line-strip', 'triangles', " + "'triangle-strip' and 'triangle-fan'. Defaulting to 'triangles'."); state.primitive = gl.TRIANGLES; state.primitiveName = cfg.primitive; } if (cfg.positions) { if (this._state.compressGeometry) { const bounds = geometryCompressionUtils.getPositionsBounds(cfg.positions); const result = geometryCompressionUtils.compressPositions(cfg.positions, bounds.min, bounds.max); state.positions = result.quantized; state.positionsDecodeMatrix = result.decodeMatrix; } else { state.positions = cfg.positions.constructor === Float32Array ? cfg.positions : new Float32Array(cfg.positions); } } if (cfg.colors) { state.colors = cfg.colors.constructor === Float32Array ? cfg.colors : new Float32Array(cfg.colors); } if (cfg.uv) { if (this._state.compressGeometry) { const bounds = geometryCompressionUtils.getUVBounds(cfg.uv); const result = geometryCompressionUtils.compressUVs(cfg.uv, bounds.min, bounds.max); state.uv = result.quantized; state.uvDecodeMatrix = result.decodeMatrix; } else { state.uv = cfg.uv.constructor === Float32Array ? cfg.uv : new Float32Array(cfg.uv); } } if (cfg.normals) { if (this._state.compressGeometry) { state.normals = geometryCompressionUtils.compressNormals(cfg.normals); } else { state.normals = cfg.normals.constructor === Float32Array ? cfg.normals : new Float32Array(cfg.normals); } } if (cfg.indices) { state.indices = (cfg.indices.constructor === Uint32Array || cfg.indices.constructor === Uint16Array) ? cfg.indices : new Uint32Array(cfg.indices); if (this._state.primitiveName === "triangles") { this._numTriangles = (cfg.indices.length / 3); } } this._buildHash(); memoryStats.meshes++; if (state.indices) { state.indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, state.indices, state.indices.length, 1, gl.STATIC_DRAW); memoryStats.indices += state.indicesBuf.numItems; } if (state.positions) { state.positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.positions, state.positions.length, 3, gl.STATIC_DRAW); memoryStats.positions += state.positionsBuf.numItems; } if (state.normals) { let normalized = state.compressGeometry; state.normalsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.normals, state.normals.length, 3, gl.STATIC_DRAW, normalized); memoryStats.normals += state.normalsBuf.numItems; } if (state.colors) { state.colorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.colors, state.colors.length, 4, gl.STATIC_DRAW); memoryStats.colors += state.colorsBuf.numItems; } if (state.uv) { state.uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, state.uv, state.uv.length, 2, gl.STATIC_DRAW); memoryStats.uvs += state.uvBuf.numItems; } } _buildHash() { const state = this._state; const hash = ["/g"]; hash.push("/" + state.primitive + ";"); if (state.positions) { hash.push("p"); } if (state.colors) { hash.push("c"); } if (state.normals || state.autoVertexNormals) { hash.push("n"); } if (state.uv) { hash.push("u"); } if (state.compressGeometry) { hash.push("cp"); } hash.push(";"); state.hash = hash.join(""); } _getEdgeIndices() { if (!this._edgeIndicesBuf) { this._buildEdgeIndices(); } return this._edgeIndicesBuf; } _getPickTrianglePositions() { if (!this._pickTrianglePositionsBuf) { this._buildPickTriangleVBOs(); } return this._pickTrianglePositionsBuf; } _getPickTriangleColors() { if (!this._pickTriangleColorsBuf) { this._buildPickTriangleVBOs(); } return this._pickTriangleColorsBuf; } _buildEdgeIndices() { // FIXME: Does not adjust indices after other objects are deleted from vertex buffer!! const state = this._state; if (!state.positions || !state.indices) { return; } const gl = this.scene.canvas.gl; const edgeIndices = buildEdgeIndices(state.positions, state.indices, state.positionsDecodeMatrix, this._edgeThreshold); this._edgeIndicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, edgeIndices, edgeIndices.length, 1, gl.STATIC_DRAW); memoryStats.indices += this._edgeIndicesBuf.numItems; } _buildPickTriangleVBOs() { // Builds positions and indices arrays that allow each triangle to have a unique color const state = this._state; if (!state.positions || !state.indices) { return; } const gl = this.scene.canvas.gl; const arrays = math.buildPickTriangles(state.positions, state.indices, state.compressGeometry); const positions = arrays.positions; const colors = arrays.colors; this._pickTrianglePositionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW); this._pickTriangleColorsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, colors, colors.length, 4, gl.STATIC_DRAW, true); memoryStats.positions += this._pickTrianglePositionsBuf.numItems; memoryStats.colors += this._pickTriangleColorsBuf.numItems; } _buildPickVertexVBOs() { // var state = this._state; // if (!state.positions || !state.indices) { // return; // } // var gl = this.scene.canvas.gl; // var arrays = math.buildPickVertices(state.positions, state.indices, state.compressGeometry); // var pickVertexPositions = arrays.positions; // var pickColors = arrays.colors; // this._pickVertexPositionsBuf = new xeokit.renderer.ArrayBuf(gl, gl.ARRAY_BUFFER, pickVertexPositions, pickVertexPositions.length, 3, gl.STATIC_DRAW); // this._pickVertexColorsBuf = new xeokit.renderer.ArrayBuf(gl, gl.ARRAY_BUFFER, pickColors, pickColors.length, 4, gl.STATIC_DRAW, true); // memoryStats.positions += this._pickVertexPositionsBuf.numItems; // memoryStats.colors += this._pickVertexColorsBuf.numItems; } /** * Gets the Geometry's primitive type. Valid types are: 'points', 'lines', 'line-loop', 'line-strip', 'triangles', 'triangle-strip' and 'triangle-fan'. @property primitive @default "triangles" @type {String} */ get primitive() { return this._state.primitiveName; } /** Indicates if this Geometry is quantized. Compression is an internally-performed optimization which stores positions, colors, normals and UVs in quantized and oct-encoded formats for reduced memory footprint and GPU bus usage. Quantized geometry may not be updated. @property compressGeometry @default false @type {Boolean} @final */ get compressGeometry() { return this._state.compressGeometry; } /** The Geometry's vertex positions. @property positions @default null @type {Number[]} */ get positions() { if (!this._state.positions) { return null; } if (!this._state.compressGeometry) { return this._state.positions; } if (!this._decompressedPositions) { this._decompressedPositions = new Float32Array(this._state.positions.length); geometryCompressionUtils.decompressPositions(this._state.positions, this._state.positionsDecodeMatrix, this._decompressedPositions); } return this._decompressedPositions; } set positions(newPositions) { const state = this._state; const positions = state.positions; if (!positions) { this.error("can't update geometry positions - geometry has no positions"); return; } if (positions.length !== newPositions.length) { this.error("can't update geometry positions - new positions are wrong length"); return; } if (this._state.compressGeometry) { const bounds = geometryCompressionUtils.getPositionsBounds(newPositions); const result = geometryCompressionUtils.compressPositions(newPositions, bounds.min, bounds.max); newPositions = result.quantized; // TODO: Copy in-place state.positionsDecodeMatrix = result.decodeMatrix; } positions.set(newPositions); if (state.positionsBuf) { state.positionsBuf.setData(positions); } this._setAABBDirty(); this.glRedraw(); } /** The Geometry's vertex normals. @property normals @default null @type {Number[]} */ get normals() { if (!this._state.normals) { return; } if (!this._state.compressGeometry) { return this._state.normals; } if (!this._decompressedNormals) { const lenCompressed = this._state.normals.length; const lenDecompressed = lenCompressed + (lenCompressed / 2); // 2 -> 3 this._decompressedNormals = new Float32Array(lenDecompressed); geometryCompressionUtils.decompressNormals(this._state.normals, this._decompressedNormals); } return this._decompressedNormals; } set normals(newNormals) { if (this._state.compressGeometry) { this.error("can't update geometry normals - quantized geometry is immutable"); // But will be eventually return; } const state = this._state; const normals = state.normals; if (!normals) { this.error("can't update geometry normals - geometry has no normals"); return; } if (normals.length !== newNormals.length) { this.error("can't update geometry normals - new normals are wrong length"); return; } normals.set(newNormals); if (state.normalsBuf) { state.normalsBuf.setData(normals); } this.glRedraw(); } /** The Geometry's UV coordinates. @property uv @default null @type {Number[]} */ get uv() { if (!this._state.uv) { return null; } if (!this._state.compressGeometry) { return this._state.uv; } if (!this._decompressedUV) { this._decompressedUV = new Float32Array(this._state.uv.length); geometryCompressionUtils.decompressUVs(this._state.uv, this._state.uvDecodeMatrix, this._decompressedUV); } return this._decompressedUV; } set uv(newUV) { if (this._state.compressGeometry) { this.error("can't update geometry UVs - quantized geometry is immutable"); // But will be eventually return; } const state = this._state; const uv = state.uv; if (!uv) { this.error("can't update geometry UVs - geometry has no UVs"); return; } if (uv.length !== newUV.length) { this.error("can't update geometry UVs - new UVs are wrong length"); return; } uv.set(newUV); if (state.uvBuf) { state.uvBuf.setData(uv); } this.glRedraw(); } /** The Geometry's vertex colors. @property colors @default null @type {Number[]} */ get colors() { return this._state.colors; } set colors(newColors) { if (this._state.compressGeometry) { this.error("can't update geometry colors - quantized geometry is immutable"); // But will be eventually return; } const state = this._state; const colors = state.colors; if (!colors) { this.error("can't update geometry colors - geometry has no colors"); return; } if (colors.length !== newColors.length) { this.error("can't update geometry colors - new colors are wrong length"); return; } colors.set(newColors); if (state.colorsBuf) { state.colorsBuf.setData(colors); } this.glRedraw(); } /** The Geometry's indices. If ````xeokit.WEBGL_INFO.SUPPORTED_EXTENSIONS["OES_element_index_uint"]```` is true, then this can be a ````Uint32Array````, otherwise it needs to be a ````Uint16Array````. @property indices @default null @type Uint16Array | Uint32Array @final */ get indices() { return this._state.indices; } /** * Local-space axis-aligned 3D boundary (AABB) of this geometry. * * The AABB is represented by a six-element Float64Array containing the min/max extents of the * axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````. * * @property aabb * @final * @type {Number[]} */ get aabb() { if (this._aabbDirty) { if (!this._aabb) { this._aabb = math.AABB3(); } math.positions3ToAABB3(this._state.positions, this._aabb, this._state.positionsDecodeMatrix); this._aabbDirty = false; } return this._aabb; } /** * Local-space oriented 3D boundary (OBB) of this geometry. * * The OBB is represented by a 32-element Float64Array containing the eight vertices of the box, * where each vertex is a homogeneous coordinate having [x,y,z,w] elements. * * @property obb * @final * @type {Number[]} */ get obb() { if (this._obbDirty) { if (!this._obb) { this._obb = math.OBB3(); } math.positions3ToAABB3(this._state.positions, tempAABB, this._state.positionsDecodeMatrix); math.AABB3ToOBB3(tempAABB, this._obb); this._obbDirty = false; } return this._obb; } _getMetrics() { if (! ("_metrics" in this)) { switch (this._state.primitiveName) { case "solid": case "surface": case "triangles": { const indices = this._state.indices; const positions = this._state.positions; const getPos = (i, out) => { const idx = indices[i] * 3; for (let j = 0; j < 3; ++j) { out[j] = positions[idx + j]; }; return out; }; const tmp = [ math.vec3(), math.vec3(), math.vec3(), math.vec3() ]; let totalArea = 0; const centroid = math.vec3([ 0, 0, 0 ]); for (let i = 0; i < indices.length; i += 3) { const v0 = getPos(i, tmp[0]); const v1 = getPos(i+1, tmp[1]); const v2 = getPos(i+2, tmp[2]); math.addVec3(v0, v1, tmp[3]); math.addVec3(v2, tmp[3], tmp[3]); const faceArea = math.lenVec3( math.cross3Vec3( math.subVec3(v1, v0, tmp[1]), math.subVec3(v2, v0, tmp[2]), tmp[0])) / 2; totalArea += faceArea; math.mulVec3Scalar(tmp[3], faceArea, tmp[3]); math.addVec3(centroid, tmp[3], centroid); } this._metrics = { surfaceArea: totalArea, centroid: math.mulVec3Scalar(centroid, 1 / totalArea / 3, centroid) }; break; } default: this._metrics = { surfaceArea: 0 }; break; } } return this._metrics; } /** * Returns the surface area of this Mesh. * @returns {number} */ get surfaceArea() { return this._getMetrics().surfaceArea; } /** * Returns the centroid of this Mesh. * @returns {number} */ get centroid() { return this._getMetrics().centroid; } /** * Approximate number of triangles in this ReadableGeometry. * * Will be zero if {@link ReadableGeometry#primitive} is not 'triangles', 'triangle-strip' or 'triangle-fan'. * * @type {Number} */ get numTriangles() { return this._numTriangles; } _setAABBDirty() { if (this._aabbDirty) { return; } this._aabbDirty = true; this._aabbDirty = true; this._obbDirty = true; } _getState() { return this._state; } /** * Destroys this ReadableGeometry */ destroy() { super.destroy(); const state = this._state; if (state.indicesBuf) { state.indicesBuf.destroy(); } if (state.positionsBuf) { state.positionsBuf.destroy(); } if (state.normalsBuf) { state.normalsBuf.destroy(); } if (state.uvBuf) { state.uvBuf.destroy(); } if (state.colorsBuf) { state.colorsBuf.destroy(); } if (this._edgeIndicesBuf) { this._edgeIndicesBuf.destroy(); } if (this._pickTrianglePositionsBuf) { this._pickTrianglePositionsBuf.destroy(); } if (this._pickTriangleColorsBuf) { this._pickTriangleColorsBuf.destroy(); } if (this._pickVertexPositionsBuf) { this._pickVertexPositionsBuf.destroy(); } if (this._pickVertexColorsBuf) { this._pickVertexColorsBuf.destroy(); } state.destroy(); memoryStats.meshes--; } } export {ReadableGeometry};