src/viewer/scene/geometry/ReadableGeometry.js
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++;
this._buildVBOs();
}
_buildVBOs() {
const state = this._state;
const gl = this.scene.canvas.gl;
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;
}
_webglContextLost() {
if (this._sceneVertexBufs) {
this._sceneVertexBufs.webglContextLost();
}
}
_webglContextRestored() {
if (this._sceneVertexBufs) {
this._sceneVertexBufs.webglContextRestored();
}
this._buildVBOs();
this._edgeIndicesBuf = null;
this._pickVertexPositionsBuf = null;
this._pickTrianglePositionsBuf = null;
this._pickTriangleColorsBuf = null;
this._pickVertexPositionsBuf = null;
this._pickVertexColorsBuf = null;
}
/**
* 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;
}
/**
* 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};