/**
A **Geometry** defines a mesh for attached {{#crossLink "Mesh"}}Meshes{{/crossLink}}.
## Usage
* [Geometry compression](#geometry-compression)
* [Geometry batching](#geometry-batching)
### Geometry compression
Geometries may be automatically quantized to reduce memory and GPU bus usage. Usually, geometry attributes such as positions
and normals are stored as 32-bit floating-point numbers. Quantization compresses those attributes to 16-bit integers
represented on a scale between the minimum and maximum values. Decompression is then done on the GPU, via a simple
matrix multiplication in the vertex shader.
#### Disabling
Since each normal vector is oct-encoded into two 8-bit unsigned integers, this can cause them to lose precision, which
may affect the accuracy of any operations that rely on them being perfectly perpendicular to their surfaces. In such
cases, you may need to disable compression for your geometries and models:
````javascript
// Disable geometry compression when loading a Model
var model = new xeogl.GLTFModel({
src: "models/gltf/modern_office/scene.gltf",
quantizeGeometry: false // Default is true
});
// Disable compression when creating a Geometry
var mesh = new xeogl.Mesh({
geometry: new xeogl.TeapotGeometry({
quantized: false // Default is false
}),
material: new xeogl.PhongMaterial({
diffuse: [0.2, 0.2, 1.0]
})
});
````
### Geometry batching
Geometries are automatically combined into the same vertex buffer objects (VBOs) so that we reduce the number of VBO
binds done by WebGL on each frame. VBO binds are expensive, so this really makes a difference when we have large numbers
of Meshes that share similar Materials (as is often the case in CAD rendering).
#### Disabling
Since combined VBOs need to be rebuilt whenever we destroy a Geometry, we can disable this optimization for individual
Models and Geometries when we know that we'll be continually creating and destroying them.
````javascript
// Disable VBO combination for a GLTFModel
var model = new xeogl.GLTFModel({
src: "models/gltf/modern_office/scene.gltf",
combinedGeometry: false // Default is true
});
// Disable VBO combination for an individual Geometry
var mesh = new xeogl.Mesh({
geometry: new xeogl.TeapotGeometry({
combined: false // Default is false
}),
material: new xeogl.PhongMaterial({
diffuse: [0.2, 0.2, 1.0]
})
});
````
@class Geometry
@module xeogl
@submodule geometry
@constructor
@param [owner] {Component} Owner component. When destroyed, the owner will destroy this component as well. Creates this component within the default {{#crossLink "Scene"}}{{/crossLink}} when omitted.
@param [cfg] {*} Configs
@param [cfg.id] {String} Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}},
generated automatically when omitted.
@param [cfg.meta] {String:Object} 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] {Array of Number} Positions array.
@param [cfg.normals] {Array of Number} Vertex normal vectors array.
@param [cfg.uv] {Array of Number} UVs array.
@param [cfg.colors] {Array of Number} Vertex colors.
@param [cfg.indices] {Array of 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.quantized=false] {Boolean} Stores positions, colors, normals and UVs in quantized and oct-encoded formats
for reduced memory footprint and GPU bus usage.
@param [cfg.combined=false] {Boolean} Combines positions, colors, normals and UVs into the same WebGL vertex buffers
with other Geometries, in order to reduce the number of buffer binds performed per frame.
@param [cfg.edgeThreshold=2] {Number} When a {{#crossLink "Mesh"}}{{/crossLink}} 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
*/
import {Component} from '../component.js';
import {State} from '../renderer/state.js';
import {ArrayBuffer} from '../renderer/arrayBuffer.js';
import {getSceneVertexBufs} from './sceneVertexBufs.js';
import {math} from '../math/math.js';
import {stats} from './../stats.js';
import {WEBGL_INFO} from './../webglInfo.js';
import {componentClasses} from "./../componentClasses.js";
const type = "xeogl.Geometry";
const memoryStats = stats.memory;
var bigIndicesSupported = WEBGL_INFO.SUPPORTED_EXTENSIONS["OES_element_index_uint"];
const IndexArrayType = bigIndicesSupported ? Uint32Array : Uint16Array;
const nullVertexBufs = new State({});
const tempAABB = math.AABB3();
class Geometry extends Component {
/**
JavaScript class name for this Component.
For example: "xeogl.AmbientLight", "xeogl.MetallicMaterial" etc.
@property type
@type String
@final
*/
get type() {
return type;
}
init(cfg) {
super.init(cfg);
const self = this;
this._state = new State({ // Arrays for emphasis effects are got from xeogl.Geometry friend methods
combined: !!cfg.combined,
quantized: !!cfg.quantized,
autoVertexNormals: !!cfg.autoVertexNormals,
primitive: null, // WebGL enum
primitiveName: null, // String
positions: null, // Uint16Array when quantized == true, else Float32Array
normals: null, // Uint8Array when quantized == true, else Float32Array
colors: null,
uv: null, // Uint8Array when quantized == true, else Float32Array
indices: null,
positionsDecodeMatrix: null, // Set when quantized == true
uvDecodeMatrix: null, // Set when quantized == true
positionsBuf: null,
normalsBuf: null,
colorsbuf: null,
uvBuf: null,
indicesBuf: null,
indicesBufCombined: null, // Indices into a shared VertexBufs, set when combined == true
hash: ""
});
this._edgeThreshold = cfg.edgeThreshold || 2.0;
// Lazy-generated VBOs
this._edgesIndicesBuf = null;
this._pickTrianglePositionsBuf = null;
this._pickTriangleColorsBuf = null;
// Local-space Boundary3D
this._boundaryDirty = 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.quantized) {
var bounds = getBounds(cfg.positions, 3);
var quantized = quantizeVec3(cfg.positions, bounds.min, bounds.max);
state.positions = quantized.quantized;
state.positionsDecodeMatrix = quantized.decode;
} 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.quantized) {
var bounds = getBounds(cfg.uv, 2);
var quantized = quantizeVec2(cfg.uv, bounds.min, bounds.max);
state.uv = quantized.quantized;
state.uvDecodeMatrix = quantized.decode;
} else {
state.uv = cfg.uv.constructor === Float32Array ? cfg.uv : new Float32Array(cfg.uv);
}
}
if (cfg.normals) {
if (this._state.quantized) {
state.normals = octEncode(cfg.normals);
} else {
state.normals = cfg.normals.constructor === Float32Array ? cfg.normals : new Float32Array(cfg.normals);
}
}
if (cfg.indices) {
if (!bigIndicesSupported && cfg.indices.constructor === Uint32Array) {
this.error("This WebGL implementation does not support Uint32Array");
return;
}
state.indices = (cfg.indices.constructor === Uint32Array || cfg.indices.constructor === Uint16Array) ? cfg.indices : new IndexArrayType(cfg.indices);
}
if (state.indices) {
state.indicesBuf = new ArrayBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, state.indices, state.indices.length, 1, gl.STATIC_DRAW);
memoryStats.indices += state.indicesBuf.numItems;
}
this._buildHash();
memoryStats.meshes++;
if (this._state.combined) {
this._sceneVertexBufs = getSceneVertexBufs(this.scene, this._state);
this._sceneVertexBufs.addGeometry(this._state);
}
this._buildVBOs();
self.fire("created", this.created = true);
}
_buildVBOs() {
const state = this._state;
const gl = this.scene.canvas.gl;
if (state.indices) {
state.indicesBuf = new ArrayBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, state.indices, state.indices.length, 1, gl.STATIC_DRAW);
memoryStats.indices += state.indicesBuf.numItems;
}
if (state.combined) {
if (state.indices) {
// indicesBufCombined is created when VertexBufs are built for this Geometry
}
} else {
if (state.positions) {
state.positionsBuf = new ArrayBuffer(gl, gl.ARRAY_BUFFER, state.positions, state.positions.length, 3, gl.STATIC_DRAW);
memoryStats.positions += state.positionsBuf.numItems;
}
if (state.normals) {
state.normalsBuf = new ArrayBuffer(gl, gl.ARRAY_BUFFER, state.normals, state.normals.length, 3, gl.STATIC_DRAW);
memoryStats.normals += state.normalsBuf.numItems;
}
if (state.colors) {
state.colorsBuf = new ArrayBuffer(gl, gl.ARRAY_BUFFER, state.colors, state.colors.length, 4, gl.STATIC_DRAW);
memoryStats.colors += state.colorsBuf.numItems;
}
if (state.uv) {
state.uvBuf = new ArrayBuffer(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.quantized) {
hash.push("cp");
}
hash.push(";");
state.hash = hash.join("");
}
_getEdgesIndices() {
if (!this._edgesIndicesBuf) {
this._buildEdgesIndices();
}
return this._edgesIndicesBuf;
}
_getPickTrianglePositions() {
if (!this._pickTrianglePositionsBuf) {
this._buildPickTriangleVBOs();
}
return this._pickTrianglePositionsBuf;
}
_getPickTriangleColors() {
if (!this._pickTriangleColorsBuf) {
this._buildPickTriangleVBOs();
}
return this._pickTriangleColorsBuf;
}
_buildEdgesIndices() { // 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 edgesIndices = buildEdgesIndices(state.positions, state.indices, state.positionsDecodeMatrix, this._edgeThreshold, state.combined);
if (state.combined) {
const indicesOffset = this._sceneVertexBufs.getIndicesOffset(state);
for (let i = 0, len = edgesIndices.length; i < len; i++) {
edgesIndices[i] += indicesOffset;
}
}
this._edgesIndicesBuf = new ArrayBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, edgesIndices, edgesIndices.length, 1, gl.STATIC_DRAW);
memoryStats.indices += this._edgesIndicesBuf.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.quantized);
const positions = arrays.positions;
const colors = arrays.colors;
this._pickTrianglePositionsBuf = new ArrayBuffer(gl, gl.ARRAY_BUFFER, positions, positions.length, 3, gl.STATIC_DRAW);
this._pickTriangleColorsBuf = new ArrayBuffer(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.quantized);
// var pickVertexPositions = arrays.positions;
// var pickColors = arrays.colors;
// this._pickVertexPositionsBuf = new xeogl.renderer.ArrayBuffer(gl, gl.ARRAY_BUFFER, pickVertexPositions, pickVertexPositions.length, 3, gl.STATIC_DRAW);
// this._pickVertexColorsBuf = new xeogl.renderer.ArrayBuffer(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._edgesIndicesBuf = null;
this._pickVertexPositionsBuf = null;
this._pickTrianglePositionsBuf = null;
this._pickTriangleColorsBuf = null;
this._pickVertexPositionsBuf = null;
this._pickVertexColorsBuf = null;
}
/**
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 quantized
@default false
@type Boolean
@final
*/
get quantized() {
return this._state.quantized;
}
/**
Indicates if this Geometry is combined.
Combination is an internally-performed optimization which combines positions, colors, normals and UVs into
the same WebGL vertex buffers with other Geometries, in order to reduce the number of buffer binds
performed per frame.
@property combined
@default false
@type Boolean
@final
*/
get combined() {
return this._state.combined;
}
/**
The Geometry's vertex positions.
@property positions
@default null
@type Float32Array
*/
get positions() {
if (!this._state.positions) {
return;
}
if (!this._state.quantized) {
return this._state.positions;
}
if (!this._decompressedPositions) {
this._decompressedPositions = new Float32Array(this._state.positions.length);
math.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.quantized) {
const bounds = getBounds(newPositions, 3);
const quantized = quantizeVec3(newPositions, bounds.min, bounds.max);
newPositions = quantized.quantized; // TODO: Copy in-place
state.positionsDecodeMatrix = quantized.decode;
}
positions.set(newPositions);
if (state.positionsBuf) {
state.positionsBuf.setData(positions);
}
if (this._state.combined) {
this._sceneVertexBufs.setPositions(state);
}
this._setBoundaryDirty();
this._renderer.imageDirty();
}
/**
The Geometry's vertex normals.
@property normals
@default null
@type Float32Array
*/
get normals() {
if (!this._state.normals) {
return;
}
if (!this._state.quantized) {
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);
math.octDecodeVec2s(this._state.normals, this._decompressedNormals);
}
return this._decompressedNormals;
}
set normals(newNormals) {
if (this._state.quantized) {
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);
}
if (this._state.combined) {
this._sceneVertexBufs.setNormals(state);
}
this._renderer.imageDirty();
}
/**
The Geometry's UV coordinates.
@property uv
@default null
@type Float32Array
*/
get uv() {
if (!this._state.uv) {
return;
}
if (!this._state.quantized) {
return this._state.uv;
}
if (!this._decompressedUV) {
this._decompressedUV = new Float32Array(this._state.uv.length);
math.decompressUVs(this._state.uv, this._state.uvDecodeMatrix, this._decompressedUV);
}
return this._decompressedUV;
}
set uv(newUV) {
if (this._state.quantized) {
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);
}
if (this._state.combined) {
this._sceneVertexBufs.setUVs(state);
}
this._renderer.imageDirty();
}
/**
The Geometry's vertex colors.
@property colors
@default null
@type Float32Array
*/
get colors() {
return this._state.colors;
}
set colors(newColors) {
if (this._state.quantized) {
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);
}
if (this._state.combined) {
this._sceneVertexBufs.setColors(state);
}
this._renderer.imageDirty();
}
/**
The Geometry's indices.
If ````xeogl.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 Float32Array containing the min/max extents of the
* axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.
*
* @property aabb
* @final
* @type {Float32Array}
*/
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 Float32Array containing the eight vertices of the box,
* where each vertex is a homogeneous coordinate having [x,y,z,w] elements.
*
* @property obb
* @final
* @type {Float32Array}
*/
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;
}
get kdtree() {
const state = this._state;
if (!state.indices || !state.positions) {
this.error("Can't provide a KD-tree: no indices/positions");
return;
}
if (!this._kdtree) {
this._kdtree = math.buildKDTree(state.indices, state.positions, this._state.positionsDecodeMatrix);
}
return this._kdtree;
}
_setBoundaryDirty() {
if (this._boundaryDirty) {
return;
}
this._boundaryDirty = true;
this._aabbDirty = true;
this._obbDirty = true;
/**
Fired whenever this Geometry's boundary changes.
Get the latest boundary from the Geometry's {{#crossLink "Geometry/aabb:property"}}{{/crossLink}}
and {{#crossLink "Geometry/obb:property"}}{{/crossLink}} properties.
@event boundary
*/
this.fire("boundary");
}
_getState() {
return this._state;
}
_getVertexBufs() {
return this._state && this._state.combined ? this._sceneVertexBufs.getVertexBufs(this._state) : nullVertexBufs;
}
destroy() {
super.destroy();
const state = this._state;
if (state.indicesBuf) {
state.indicesBuf.destroy();
}
if (this._edgesIndicesBuf) {
this._edgesIndicesBuf.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();
}
if (this._state.combined) {
this._sceneVertexBufs.removeGeometry(state);
}
state.destroy();
memoryStats.meshes--;
}
}
function getBounds(array, stride) {
const min = new Float32Array(stride);
const max = new Float32Array(stride);
let i, j;
for (i = 0; i < stride; i++) {
min[i] = Number.MAX_VALUE;
max[i] = -Number.MAX_VALUE;
}
for (i = 0; i < array.length; i += stride) {
for (j = 0; j < stride; j++) {
min[j] = Math.min(min[j], array[i + j]);
max[j] = Math.max(max[j], array[i + j]);
}
}
return {
min: min,
max: max
};
}
// http://cg.postech.ac.kr/research/mesh_comp_mobile/mesh_comp_mobile_conference.pdf
var quantizeVec3 = (function () {
const translate = math.mat4();
const scale = math.mat4();
return function (array, min, max) {
const quantized = new Uint16Array(array.length);
const multiplier = new Float32Array([
65535 / (max[0] - min[0]),
65535 / (max[1] - min[1]),
65535 / (max[2] - min[2])
]);
let i;
for (i = 0; i < array.length; i += 3) {
quantized[i + 0] = Math.floor((array[i + 0] - min[0]) * multiplier[0]);
quantized[i + 1] = Math.floor((array[i + 1] - min[1]) * multiplier[1]);
quantized[i + 2] = Math.floor((array[i + 2] - min[2]) * multiplier[2]);
}
math.identityMat4(translate);
math.translationMat4v(min, translate);
math.identityMat4(scale);
math.scalingMat4v([
(max[0] - min[0]) / 65535,
(max[1] - min[1]) / 65535,
(max[2] - min[2]) / 65535
], scale);
const decodeMat = math.mulMat4(translate, scale, math.identityMat4());
return {
quantized: quantized,
decode: decodeMat
};
};
})();
var quantizeVec2 = (function () {
const translate = math.mat3();
const scale = math.mat3();
return function (array, min, max) {
const quantized = new Uint16Array(array.length);
const multiplier = new Float32Array([
65535 / (max[0] - min[0]),
65535 / (max[1] - min[1])
]);
let i;
for (i = 0; i < array.length; i += 2) {
quantized[i + 0] = Math.floor((array[i + 0] - min[0]) * multiplier[0]);
quantized[i + 1] = Math.floor((array[i + 1] - min[1]) * multiplier[1]);
}
math.identityMat3(translate);
math.translationMat3v(min, translate);
math.identityMat3(scale);
math.scalingMat3v([
(max[0] - min[0]) / 65535,
(max[1] - min[1]) / 65535
], scale);
const decodeMat = math.mulMat3(translate, scale, math.identityMat3());
return {
quantized: quantized,
decode: decodeMat
};
};
})();
// http://jcgt.org/published/0003/02/01/
function octEncode(array) {
const encoded = new Int8Array(array.length * 2 / 3);
let oct, dec, best, currentCos, bestCos;
let i, ei;
for (i = 0, ei = 0; i < array.length; i += 3, ei += 2) {
// Test various combinations of ceil and floor
// to minimize rounding errors
best = oct = octEncodeVec3(array, i, "floor", "floor");
dec = octDecodeVec2(oct);
currentCos = bestCos = dot(array, i, dec);
oct = octEncodeVec3(array, i, "ceil", "floor");
dec = octDecodeVec2(oct);
currentCos = dot(array, i, dec);
if (currentCos > bestCos) {
best = oct;
bestCos = currentCos;
}
oct = octEncodeVec3(array, i, "floor", "ceil");
dec = octDecodeVec2(oct);
currentCos = dot(array, i, dec);
if (currentCos > bestCos) {
best = oct;
bestCos = currentCos;
}
oct = octEncodeVec3(array, i, "ceil", "ceil");
dec = octDecodeVec2(oct);
currentCos = dot(array, i, dec);
if (currentCos > bestCos) {
best = oct;
bestCos = currentCos;
}
encoded[ei] = best[0];
encoded[ei + 1] = best[1];
}
return encoded;
}
// Oct-encode single normal vector in 2 bytes
function octEncodeVec3(array, i, xfunc, yfunc) {
let x = array[i] / (Math.abs(array[i]) + Math.abs(array[i + 1]) + Math.abs(array[i + 2]));
let y = array[i + 1] / (Math.abs(array[i]) + Math.abs(array[i + 1]) + Math.abs(array[i + 2]));
if (array[i + 2] < 0) {
let tempx = x;
let tempy = y;
tempx = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
tempy = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
x = tempx;
y = tempy;
}
return new Int8Array([
Math[xfunc](x * 127.5 + (x < 0 ? -1 : 0)),
Math[yfunc](y * 127.5 + (y < 0 ? -1 : 0))
]);
}
// Decode an oct-encoded normal
function octDecodeVec2(oct) {
let x = oct[0];
let y = oct[1];
x /= x < 0 ? 127 : 128;
y /= y < 0 ? 127 : 128;
const z = 1 - Math.abs(x) - Math.abs(y);
if (z < 0) {
x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
y = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1);
}
const length = Math.sqrt(x * x + y * y + z * z);
return [
x / length,
y / length,
z / length
];
}
// Dot product of a normal in an array against a candidate decoding
function dot(array, i, vec3) {
return array[i] * vec3[0] + array[i + 1] * vec3[1] + array[i + 2] * vec3[2];
}
var buildEdgesIndices = (function () {
// TODO: Optimize with caching, but need to cater to both compressed and uncompressed positions
const uniquePositions = [];
const indicesLookup = [];
const indicesReverseLookup = [];
const weldedIndices = [];
function weldVertices(positions, indices) {
const positionsMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique)
let vx;
let vy;
let vz;
let key;
const precisionPoints = 4; // number of decimal points, e.g. 4 for epsilon of 0.0001
const precision = Math.pow(10, precisionPoints);
let i;
let len;
let lenUniquePositions = 0;
for (i = 0, len = positions.length; i < len; i += 3) {
vx = positions[i];
vy = positions[i + 1];
vz = positions[i + 2];
key = Math.round(vx * precision) + '_' + Math.round(vy * precision) + '_' + Math.round(vz * precision);
if (positionsMap[key] === undefined) {
positionsMap[key] = lenUniquePositions / 3;
uniquePositions[lenUniquePositions++] = vx;
uniquePositions[lenUniquePositions++] = vy;
uniquePositions[lenUniquePositions++] = vz;
}
indicesLookup[i / 3] = positionsMap[key];
}
for (i = 0, len = indices.length; i < len; i++) {
weldedIndices[i] = indicesLookup[indices[i]];
indicesReverseLookup[weldedIndices[i]] = indices[i];
}
}
const faces = [];
let numFaces = 0;
const compa = new Uint16Array(3);
const compb = new Uint16Array(3);
const compc = new Uint16Array(3);
const a = math.vec3();
const b = math.vec3();
const c = math.vec3();
const cb = math.vec3();
const ab = math.vec3();
const cross = math.vec3();
const normal = math.vec3();
function buildFaces(numIndices, positionsDecodeMatrix) {
numFaces = 0;
for (let i = 0, len = numIndices; i < len; i += 3) {
const ia = ((weldedIndices[i]) * 3);
const ib = ((weldedIndices[i + 1]) * 3);
const ic = ((weldedIndices[i + 2]) * 3);
if (positionsDecodeMatrix) {
compa[0] = uniquePositions[ia];
compa[1] = uniquePositions[ia + 1];
compa[2] = uniquePositions[ia + 2];
compb[0] = uniquePositions[ib];
compb[1] = uniquePositions[ib + 1];
compb[2] = uniquePositions[ib + 2];
compc[0] = uniquePositions[ic];
compc[1] = uniquePositions[ic + 1];
compc[2] = uniquePositions[ic + 2];
// Decode
math.decompressPosition(compa, positionsDecodeMatrix, a);
math.decompressPosition(compb, positionsDecodeMatrix, b);
math.decompressPosition(compc, positionsDecodeMatrix, c);
} else {
a[0] = uniquePositions[ia];
a[1] = uniquePositions[ia + 1];
a[2] = uniquePositions[ia + 2];
b[0] = uniquePositions[ib];
b[1] = uniquePositions[ib + 1];
b[2] = uniquePositions[ib + 2];
c[0] = uniquePositions[ic];
c[1] = uniquePositions[ic + 1];
c[2] = uniquePositions[ic + 2];
}
math.subVec3(c, b, cb);
math.subVec3(a, b, ab);
math.cross3Vec3(cb, ab, cross);
math.normalizeVec3(cross, normal);
const face = faces[numFaces] || (faces[numFaces] = {normal: math.vec3()});
face.normal[0] = normal[0];
face.normal[1] = normal[1];
face.normal[2] = normal[2];
numFaces++;
}
}
return function (positions, indices, positionsDecodeMatrix, edgeThreshold, combined) {
weldVertices(positions, indices);
buildFaces(indices.length, positionsDecodeMatrix);
const edgeIndices = [];
const thresholdDot = Math.cos(math.DEGTORAD * edgeThreshold);
const edges = {};
let edge1;
let edge2;
let index1;
let index2;
let key;
let largeIndex = false;
let edge;
let normal1;
let normal2;
let dot;
let ia;
let ib;
for (let i = 0, len = indices.length; i < len; i += 3) {
const faceIndex = i / 3;
for (let j = 0; j < 3; j++) {
edge1 = weldedIndices[i + j];
edge2 = weldedIndices[i + ((j + 1) % 3)];
index1 = Math.min(edge1, edge2);
index2 = Math.max(edge1, edge2);
key = index1 + "," + index2;
if (edges[key] === undefined) {
edges[key] = {
index1: index1,
index2: index2,
face1: faceIndex,
face2: undefined
};
} else {
edges[key].face2 = faceIndex;
}
}
}
for (key in edges) {
edge = edges[key];
// an edge is only rendered if the angle (in degrees) between the face normals of the adjoining faces exceeds this value. default = 1 degree.
if (edge.face2 !== undefined) {
normal1 = faces[edge.face1].normal;
normal2 = faces[edge.face2].normal;
dot = math.dotVec3(normal1, normal2);
if (dot > thresholdDot) {
continue;
}
}
ia = indicesReverseLookup[edge.index1];
ib = indicesReverseLookup[edge.index2];
if (!largeIndex && ia > 65535 || ib > 65535) {
largeIndex = true;
}
edgeIndices.push(ia);
edgeIndices.push(ib);
}
return (largeIndex || combined) ? new Uint32Array(edgeIndices) : new Uint16Array(edgeIndices);
};
})();
componentClasses[type] = Geometry;
export {Geometry};