@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
1,029 lines (892 loc) • 48.9 kB
JavaScript
import {ENTITY_FLAGS} from "../ENTITY_FLAGS.js";
import {getColSilhEdgePickFlags, getRenderers, Layer} from "./Layer.js";
import {math} from "../../math/math.js";
import {quantizePositions, transformAndOctEncodeNormals} from "../compression.js";
import {geometryCompressionUtils} from "../../math/geometryCompressionUtils.js";
const tempFloat32 = new Float32Array(1);
const tempFloat32Vec4 = new Float32Array(4);
const tempVec3fa = new Float32Array(3);
const tempMat4 = math.mat4();
const tempVec4a = math.vec4([0, 0, 0, 1]);
const tempVec4b = math.vec4();
const tempVec3a = math.vec3();
const tempVec3b = math.vec3();
const tempVec3c = math.vec3();
const tempVec3d = math.vec3();
const tempVec3e = math.vec3();
const tempVec3f = math.vec3();
const tempVec3g = math.vec3();
const tempUint8Array4 = new Uint8Array(4);
const matricesUniformBlockBufferData = new Float32Array(4 * 4 * 6); // there is 6 mat4
const iota = function(n) {
const ret = [ ];
for (let i = 0; i < n; ++i) ret.push(i);
return ret;
};
// fills the whole dst array with src copies
const fillArray = function(dst, src) {
dst.set(src);
let soFar = src.length;
const allDataLen = dst.length;
while (soFar < allDataLen) {
const toCopy = Math.min(soFar, allDataLen - soFar);
dst.set(dst.subarray(0, toCopy), soFar);
soFar += toCopy;
}
};
const scratchMemory = (function() {
/**
* Provides scratch memory for methods like TrianglesBatchingLayer setFlags() and setColors(),
* so they don't need to allocate temporary arrays that need garbage collection.
*/
let cnt = 0;
const arrays = new Map();
return {
acquire: () => cnt++,
release: () => {
cnt--;
if (cnt === 0) {
arrays.clear();
}
},
getTypeArray: function(type, len) {
if (! arrays.has(type)) {
arrays.set(type, { });
}
const typeArrays = arrays.get(type);
if (! (len in typeArrays)) {
typeArrays[len] = new type(len);
}
return typeArrays[len];
}
};
})();
/**
* @private
*/
export class VBOLayer extends Layer {
/**
* @param model
* @param primitive
* @param origin
*
* @param cfg
* @param cfg.textureSet
*
* batching:
* @param cfg.positionsDecodeMatrix
* @param cfg.uvDecodeMatrix
* @param cfg.maxGeometryBatchSize
*
* instancing:
* @param cfg.geometry
*/
constructor(model, primitive, origin, cfg) {
super(model, primitive, origin);
const instancing = !! cfg.geometry;
this._maxVerts = cfg.maxGeometryBatchSize;
const positionsDecodeMatrix = instancing ? cfg.geometry.positionsDecodeMatrix : cfg.positionsDecodeMatrix;
this._positionsDecodeMatrix = positionsDecodeMatrix && math.mat4(positionsDecodeMatrix);
this._cfgUvDecodeMatrix = cfg.uvDecodeMatrix && math.mat3(cfg.uvDecodeMatrix);
this._instancedGeometry = cfg.geometry;
this._textureSet = cfg.textureSet;
this._modelAABB = (! instancing) && math.collapseAABB3(); // Model-space AABB
this._portions = [ ];
const attribute = function() {
let length = 0;
const portions = [ ];
return {
length: () => length,
append: function(data, times = 1, denormalizeScale = 1.0, increment = 0.0) {
length += times * data.length;
portions.push({ data: data, times: times, denormalizeScale: denormalizeScale, increment: increment });
},
compileBuffer: function(type) {
let len = 0;
portions.forEach(p => { len += p.times * p.data.length; });
const buf = new type(len);
let begin = 0;
portions.forEach(p => {
const data = p.data;
const dScale = p.denormalizeScale;
const increment = p.increment;
const subBuf = buf.subarray(begin);
if ((dScale === 1.0) && (increment === 0.0)) {
subBuf.set(data);
} else {
for (let i = 0; i < data.length; ++i) {
subBuf[i] = increment + data[i] * dScale;
}
}
let soFar = data.length;
const allDataLen = p.times * data.length;
while (soFar < allDataLen) {
const toCopy = Math.min(soFar, allDataLen - soFar);
subBuf.set(subBuf.subarray(0, toCopy), soFar);
soFar += toCopy;
}
begin += soFar;
});
return buf;
},
_clearToOptimizeGC: () => { portions.length = 0; }
};
};
this._buffer = {
colors: attribute(), // in instancing used only for non-points
metallicRoughness: attribute(), // used for triangulated
pickColors: attribute(), // used for non-lines
...(instancing
? {
// Modeling matrix per instance, array for each column
modelMatrixCol: [ attribute(), attribute(), attribute() ],
modelNormalMatrixCol: [ attribute(), attribute(), attribute() ]
}
: {
positions: attribute(),
indices: attribute(), // used for non-points
uv: attribute(), // used for triangulated
normals: attribute(), // used for triangulated
edgeIndices: attribute(), // used for triangulated
})
};
}
/**
* Tests if there is room for another portion in this Layer.
*
* @param lenPositions Number of positions we'd like to create in the portion.
* @param lenIndices Number of indices we'd like to create in this portion.
* @returns {Boolean} True if OK to create another portion.
*/
canCreatePortion(lenPositions, lenIndices) {
return this._instancedGeometry || (((this._buffer.positions.length() + lenPositions) <= (this._maxVerts * 3)) && ((this._buffer.indices.length() + lenIndices) <= (this._maxVerts * 3)));
}
/**
* Creates a new portion within this Layer, returns the new portion ID.
*
* @param mesh The SceneModelMesh that owns the portion
* @param cfg Portion params
* @param cfg.metallic Metalness factor [0..255] (triangulated)
* @param cfg.roughness Roughness factor [0..255] (triangulated)
* @param cfg.pickColor Quantized pick color (non-lines)
* @param cfg.meshMatrix Flat float 4x4 matrix (optional in batching)
* batching:
* @param cfg.positionsCompressed Flat quantized positions array - decompressed with positionsDecodeMatrix
* @param cfg.positions Flat float Local-space positions array.
* @param cfg.indices Flat int indices array.
* @param [cfg.normalsCompressed]
* @param [cfg.normals] Flat float normals array (triangulated)
* @param [cfg.colors] Flat float colors array (non-lines)
* @param [cfg.colorsCompressed] Quantized RGB colors [0..255,0..255,0..255,0..255] (non-lines)
* @param cfg.color Float RGB color [0..1,0..1,0..1] (points) or Quantized RGB color [0..255,0..255,0..255,0..255] (non-points)
* @param cfg.opacity Opacity [0..255] (non-points)
* @param [cfg.uv] Flat UVs array (triangulated)
* @param [cfg.uvCompressed] (triangulated)
* @param [cfg.edgeIndices] Flat int edges indices array (triangulated)
* @param cfg.aabb Flat float AABB World-space AABB
* instancing:
* @param cfg.color Color [0..255,0..255,0..255]
* @param cfg.opacity Opacity [0..255].
* @returns {number} Portion ID
*/
createPortion(mesh, cfg) {
const buffer = this._buffer;
const instancedGeometry = this._instancedGeometry;
const model = this.model;
const portionId = this._portions.length;
const primitive = this.primitive;
const useCompressed = !! this._positionsDecodeMatrix;
const scene = model.scene;
const meshMatrix = cfg.meshMatrix;
const appendPortion = (portionBase, portionSize, indices, quantizedPositions, meshMatrix) => {
const prevPortion = (this._portions.length > 0) && this._portions[this._portions.length - 1];
const portion = {
indicesBaseIndex: prevPortion ? (prevPortion.indicesBaseIndex + prevPortion.numIndices) : 0,
numIndices: indices ? indices.length : 0,
portionBase: portionBase,
portionSize: portionSize,
retainedGeometry: scene.readableGeometryEnabled && (primitive !== "points") && (primitive !== "lines") && {
indices: indices,
quantizedPositions: quantizedPositions,
offset: scene.entityOffsetsEnabled && math.vec3(),
matrix: meshMatrix && meshMatrix.slice(),
inverseMatrix: null, // Lazy-computed for instancing in precisionRayPickSurface
normalMatrix: null, // Lazy-computed for instancing in precisionRayPickSurface
}
};
if ((primitive !== "points") && (primitive !== "lines")) {
buffer.metallicRoughness.append([ cfg.metallic ?? 0, cfg.roughness ?? 255 ], portionSize);
}
if (primitive !== "lines") {
buffer.pickColors.append(cfg.pickColor.slice(0, 4), portionSize);
}
this._portions.push(portion);
model.numPortions++;
this._meshes.push(mesh);
return portionId;
};
if (instancedGeometry) {
buffer.modelMatrixCol.forEach((b, i) => b.append([ meshMatrix[i+0], meshMatrix[i+4], meshMatrix[i+8], meshMatrix[i+12] ]));
if (primitive !== "points") {
const color = cfg.color; // Color is pre-quantized by SceneModel
buffer.colors.append([ color[0], color[1], color[2], cfg.opacity ?? 255 ]);
}
if ((primitive !== "points") && (primitive !== "lines")) {
if (instancedGeometry.normals) {
// Note: order of inverse and transpose doesn't matter
const normalMatrix = math.inverseMat4(math.transposeMat4(meshMatrix, math.mat4()));
buffer.modelNormalMatrixCol.forEach((b, i) => b.append([ normalMatrix[i+0], normalMatrix[i+4], normalMatrix[i+8], normalMatrix[i+12] ]));
}
}
return appendPortion(portionId, 1, instancedGeometry.indices, instancedGeometry.positionsCompressed, meshMatrix);
} else {
const vertsBaseIndex = buffer.positions.length() / 3;
const positions = useCompressed ? cfg.positionsCompressed : cfg.positions;
if (! positions) {
throw ((useCompressed ? "positionsCompressed" : "positions") + " expected");
}
buffer.positions.append(positions);
const numVerts = positions.length / 3;
const indices = cfg.indices;
if (indices) {
buffer.indices.append(indices, 1, 1.0, vertsBaseIndex);
}
const normalsCompressed = cfg.normalsCompressed;
const normals = cfg.normals;
if (normalsCompressed && normalsCompressed.length > 0) {
buffer.normals.append(normalsCompressed);
} else if (normals && normals.length > 0) {
const worldNormalMatrix = tempMat4;
if (meshMatrix) {
math.transposeMat4(meshMatrix, worldNormalMatrix);
math.inverseMat4(worldNormalMatrix, worldNormalMatrix); // Note: order of inverse and transpose doesn't matter
} else {
math.identityMat4(worldNormalMatrix);
}
const normalsData = [ ];
transformAndOctEncodeNormals(worldNormalMatrix, normals, normals.length, normalsData, 0);
buffer.normals.append(normalsData);
}
const colors = cfg.colors;
const colorsCompressed = cfg.colorsCompressed;
const color = cfg.color;
if (colors) {
if (primitive === "points") {
buffer.colors.append(colors, 1, 255.0);
} else { // triangulated
const colorsData = [ ];
for (let i = 0, len = colors.length; i < len; i += 3) {
colorsData.push(colors[i] * 255);
colorsData.push(colors[i + 1] * 255);
colorsData.push(colors[i + 2] * 255);
colorsData.push(255);
}
buffer.colors.append(colorsData);
}
} else if (colorsCompressed) {
if (primitive === "points") {
buffer.colors.append(colorsCompressed);
} else { // triangulated
const colorsData = [ ];
for (let i = 0, len = colorsCompressed.length; i < len; i += 3) {
colorsData.push(colorsCompressed[i]);
colorsData.push(colorsCompressed[i + 1]);
colorsData.push(colorsCompressed[i + 2]);
colorsData.push(255);
}
buffer.colors.append(colorsData);
}
} else if (color) {
// Color is pre-quantized by VBOSceneModel
buffer.colors.append([ color[0], color[1], color[2], (primitive === "points") ? 1.0 : cfg.opacity ], numVerts);
}
const nonEmpty = v => v && (v.length > 0) && v;
const uv = nonEmpty(cfg.uv) || nonEmpty(cfg.uvCompressed);
if (uv) {
buffer.uv.append(uv);
}
const edgeIndices = cfg.edgeIndices;
if (edgeIndices) {
buffer.edgeIndices.append(edgeIndices, 1, 1.0, vertsBaseIndex);
}
math.expandAABB3(this._modelAABB, cfg.aabb);
// quantizedPositions are initialized in finalize()
return appendPortion(vertsBaseIndex, numVerts, indices);
}
}
compilePortions() {
const buffer = this._buffer;
const instancedGeometry = this._instancedGeometry;
const model = this.model;
const modelAABB = this._modelAABB;
const origin = this.origin;
const portions = this._portions;
const primitive = this.primitive;
const textureSet = this._textureSet;
let positionsDecodeMatrix = this._positionsDecodeMatrix;
const cfgUvDecodeMatrix = this._cfgUvDecodeMatrix;
const scene = model.scene;
const gl = scene.canvas.gl;
const instancing = !! instancedGeometry;
const cleanups = [ ];
const createGlBuffer = (target, srcData, size, usage) => {
if (srcData.length > 0) {
const srcDataConstructor = srcData.constructor;
const [byteSize, type] = ({
[Uint8Array]: [1, gl.UNSIGNED_BYTE],
[Int8Array]: [1, gl.BYTE],
[Uint16Array]: [2, gl.UNSIGNED_SHORT],
[Int16Array]: [2, gl.SHORT],
[Uint32Array]: [4, gl.UNSIGNED_INT],
[Int32Array]: [4, gl.INT],
[Float32Array]: [4, gl.FLOAT]
})[srcDataConstructor];
const buffer = gl.createBuffer();
cleanups.push(() => gl.deleteBuffer(buffer));
const bindBuffer = () => gl.bindBuffer(target, buffer);
const setData = data => {
bindBuffer();
gl.bufferData(target, data, usage);
};
setData(srcData);
const bytesPerElement = srcData.BYTES_PER_ELEMENT;
const numItems = srcData.length / size;
return {
bindBuffer: bindBuffer,
getData: (baseIndex = 0, length = numItems - baseIndex) => {
bindBuffer();
const array = new srcDataConstructor(length * size);
gl.getBufferSubData(target, baseIndex * byteSize * size, array, 0, length * size);
return array;
},
numItems: numItems,
setData: setData,
setSubData: (data, offset) => {
bindBuffer();
gl.bufferSubData(target, offset * bytesPerElement, data);
},
type: type
};
} else {
return null;
}
};
const maybeCreateBuffer = (srcData, size, usage, setDivisor = false, normalized = false) => {
const buf = createGlBuffer(gl.ARRAY_BUFFER, srcData, size, usage);
return buf && {
getData: buf.getData,
numItems: buf.numItems,
setData: buf.setData,
setSubData: buf.setSubData,
attributeDivisor: setDivisor && 1,
bindAtLocation: location => {
buf.bindBuffer();
gl.vertexAttribPointer(location, size, buf.type, !!normalized, 0, 0);
}
};
};
const maybeCreateIndicesBuffer = srcData => {
const buf = createGlBuffer(gl.ELEMENT_ARRAY_BUFFER, srcData, 1, gl.STATIC_DRAW);
return buf && {
getData: buf.getData,
bindIndicesBuffer: buf.bindBuffer,
indicesCount: srcData.length,
indicesType: buf.type
};
};
const attributesCnt = portions.reduce((acc,p) => acc + p.portionSize, 0);
const flagsBuf = maybeCreateBuffer(new Float32Array(attributesCnt), 1, gl.DYNAMIC_DRAW, instancing);
const createColorsBuf = (srcData, usage) => maybeCreateBuffer(srcData, 4, usage, instancing && (primitive !== "points"), true);
const colorsBuf = ((instancing && instancedGeometry.colorsCompressed)
? createColorsBuf(new Uint8Array(instancedGeometry.colorsCompressed), gl.STATIC_DRAW)
: createColorsBuf(buffer.colors.compileBuffer(Uint8Array), gl.DYNAMIC_DRAW));
const offsetsBuf = scene.entityOffsetsEnabled && maybeCreateBuffer(new Float32Array(attributesCnt * 3), 3, gl.DYNAMIC_DRAW, instancing);
const metallicRoughnessBuf = maybeCreateBuffer(buffer.metallicRoughness.compileBuffer(Uint8Array), 2, gl.STATIC_DRAW, instancing);
const pickColorsBuf = maybeCreateBuffer(buffer.pickColors.compileBuffer(Uint8Array), 4, gl.STATIC_DRAW, instancing);
const edgeIndicesBuf = maybeCreateIndicesBuffer(instancing
? new Uint32Array(instancedGeometry.edgeIndices)
: buffer.edgeIndices.compileBuffer(Uint32Array));
const indices = (instancing
? ((primitive !== "points") && instancedGeometry.indices && new Uint32Array(instancedGeometry.indices))
: buffer.indices.compileBuffer(Uint32Array));
const indicesBuf = indices && maybeCreateIndicesBuffer(indices);
const positions = (instancing
? instancedGeometry.positionsCompressed
: (positionsDecodeMatrix
? buffer.positions.compileBuffer(Uint16Array)
: (quantizePositions(buffer.positions.compileBuffer(Float64Array), modelAABB, positionsDecodeMatrix = math.mat4()))));
const positionsBuf = positions && maybeCreateBuffer(positions, 3, gl.STATIC_DRAW);
if (! instancing) {
for (let i = 0; i < portions.length; ++i) { // WARNING: Replacing this for-loop by forEach increases memory consumption
const portion = portions[i]; // by not GCing `positions` arr, even if portion.retainedGeometry is false.
if (portion.retainedGeometry) { // See the comment related to XCD-408 and XCD-424 below.
const start = 3 * portion.portionBase;
portion.retainedGeometry.quantizedPositions = positions.subarray(start, start + 3 * portion.portionSize);
}
}
}
const modelMatrixColBufs = instancing && buffer.modelMatrixCol.map(b => maybeCreateBuffer(b.compileBuffer(Float32Array), 4, gl.STATIC_DRAW, true));
const normals = (instancing
? false // (primitive !== "points") && (primitive !== "lines") && instancedGeometry.normalsCompressed
: buffer.normals.compileBuffer(Int8Array));
// Normals are already oct-encoded, so `normalized = true` for oct encoded UInts
const normalsBuf = normals && maybeCreateBuffer(normals, 3, gl.STATIC_DRAW, false, true);
// WARNING: modelMatrixColBufs and normalsBuf are never simultaneously defined at the moment (when instancing=true)
const modelNormalMatrixColBufs = modelMatrixColBufs && normalsBuf && buffer.modelNormalMatrixCol.map(b => maybeCreateBuffer(b.compileBuffer(Float32Array), 4, gl.STATIC_DRAW, true));
const uvSetup = (instancing
? ((primitive !== "points") && (primitive !== "lines") && instancedGeometry.uvCompressed && {
buf: maybeCreateBuffer(instancedGeometry.uvCompressed, 2, gl.STATIC_DRAW),
mat: instancedGeometry.uvDecodeMatrix
})
: (function() {
const uvs = buffer.uv.compileBuffer(Float32Array);
if (uvs.length === 0) {
return null;
} else if (cfgUvDecodeMatrix) {
return {
buf: maybeCreateBuffer(uvs, 2, gl.STATIC_DRAW),
mat: cfgUvDecodeMatrix
};
} else {
const bounds = geometryCompressionUtils.getUVBounds(uvs);
const result = geometryCompressionUtils.compressUVs(uvs, bounds.min, bounds.max);
return {
buf: maybeCreateBuffer(result.quantized, 2, gl.STATIC_DRAW),
mat: math.mat3(result.decodeMatrix)
};
}
})());
// Free up memory
// Optimization to free up memory (XCD-408 and XCD-424)
// The following line was added to assist GC in cleaning up some of the data. See also the `portions` loop above.
// A Chrome test with Lyon[1-9].xkt models loaded simultaneously showed a decrease of 538MB to 143MB.
Object.values(this._buffer).forEach(a => a._clearToOptimizeGC ? a._clearToOptimizeGC() : a.forEach(a => a._clearToOptimizeGC()));
this._buffer = null;
scratchMemory.acquire();
cleanups.push(() => scratchMemory.release());
let deferredFlagValues = null;
/**
* flags are 4bits values encoded on a 32bit base. color flag on the first 4 bits, silhouette flag on the next 4 bits and so on for edge, pick and clippable.
*/
const setFlags = (portionId, flags, transparent, deferred = false) => {
getColSilhEdgePickFlags(flags, transparent, (primitive !== "points") && (primitive !== "lines"), scene, tempUint8Array4);
const clippableFlag = !!(flags & ENTITY_FLAGS.CLIPPABLE) ? 1 : 0;
let vertFlag = 0;
vertFlag |= tempUint8Array4[0];
vertFlag |= tempUint8Array4[1] << 4;
vertFlag |= tempUint8Array4[2] << 8;
vertFlag |= tempUint8Array4[3] << 12;
vertFlag |= clippableFlag << 16;
tempFloat32[0] = vertFlag;
const portion = portions[portionId];
const firstFlag = portion.portionBase;
const lenFlags = portion.portionSize;
if (deferred && (! instancedGeometry)) {
// Avoid zillions of individual WebGL bufferSubData calls - buffer them to apply in one shot
if (!deferredFlagValues) {
deferredFlagValues = new Float32Array(attributesCnt);
}
fillArray(deferredFlagValues.subarray(firstFlag, firstFlag + lenFlags), tempFloat32);
} else if (flagsBuf) {
const tempArray = scratchMemory.getTypeArray(Float32Array, lenFlags);
fillArray(tempArray, tempFloat32);
flagsBuf.setSubData(tempArray, firstFlag);
}
};
const solid = (primitive === "solid");
return {
edgesColorOpaqueAllowed: () => true,
solid: solid,
sortId: (((primitive === "points") ? "Points" : ((primitive === "lines") ? "Lines" : "Triangles"))
+ (instancing ? "Instancing" : "Batching") + "Layer" +
(((primitive !== "points") && (primitive !== "lines"))
? ((solid ? "-solid" : "-surface")
+ "-autoNormals"
+ (instancing
? ""
// TODO: These two parts need to be IDs (ie. unique):
: ((textureSet && textureSet.colorTexture ? "-colorTexture" : "")
+
(textureSet && textureSet.metallicRoughnessTexture ? "-metallicRoughnessTexture" : ""))))
: "")),
setClippableFlags: setFlags,
setFlags: setFlags,
setFlags2: (portionId, flags, deferred) => { },
setDeferredFlags: () => {
if (deferredFlagValues) {
flagsBuf.setData(deferredFlagValues);
deferredFlagValues = null;
}
},
setColor: (portionId, color) => { // RGBA color is normalized as ints
if (colorsBuf) {
const portion = portions[portionId];
const tempArray = scratchMemory.getTypeArray(Uint8Array, portion.portionSize * 4);
// alpha used to be unset for points, so effectively random (from last use)
fillArray(tempArray, color.slice(0, 4));
colorsBuf.setSubData(tempArray, portion.portionBase * 4);
}
},
setMatrix: (portionId, matrix) => {
if (modelMatrixColBufs) {
modelMatrixColBufs.forEach((b, i) => {
tempFloat32Vec4[0] = matrix[i+0];
tempFloat32Vec4[1] = matrix[i+4];
tempFloat32Vec4[2] = matrix[i+8];
tempFloat32Vec4[3] = matrix[i+12];
b.setSubData(tempFloat32Vec4, portionId * 4);
});
}
},
setOffset: (portionId, offset) => {
if (!scene.entityOffsetsEnabled) {
model.error("Entity#offset not enabled for this Viewer"); // See Viewer entityOffsetsEnabled
} else {
const portion = portions[portionId];
if (offsetsBuf) {
tempVec3fa.set(offset);
const tempArray = scratchMemory.getTypeArray(Float32Array, portion.portionSize * 3);
fillArray(tempArray, tempVec3fa);
offsetsBuf.setSubData(tempArray, portion.portionBase * 3);
}
if (portion.retainedGeometry) {
portion.retainedGeometry.offset.set(offset);
}
}
},
getEachIndex: (portionId, callback) => {
const retainedGeometry = portions[portionId].retainedGeometry;
if (retainedGeometry) {
retainedGeometry.indices.forEach(i => callback(i));
}
},
getEachVertex: (portionId, callback) => {
const retainedGeometry = portions[portionId].retainedGeometry;
if (retainedGeometry) {
const origVec = tempVec4b;
if (origin) {
origVec.set(origin, 0);
} else {
origVec[0] = origVec[1] = origVec[2] = 0;
}
origVec[3] = 1;
const sceneModelMatrix = model.matrix;
const positions = retainedGeometry.quantizedPositions;
const worldPos = tempVec4a;
for (let i = 0, len = positions.length; i < len; i += 3) {
worldPos[0] = positions[i];
worldPos[1] = positions[i + 1];
worldPos[2] = positions[i + 2];
math.decompressPosition(worldPos, positionsDecodeMatrix);
if (retainedGeometry.matrix) {
math.transformPoint3(retainedGeometry.matrix, worldPos, worldPos);
}
worldPos[3] = 1;
math.addVec3(origVec, worldPos, worldPos);
math.mulMat4v4(sceneModelMatrix, worldPos, worldPos);
callback(worldPos);
}
}
},
readGeometryData: (primitive !== "points") && (primitive !== "lines") && ((portionId) => {
const portion = (! instancing) && portions[portionId];
const indices = (portion
? indicesBuf.getData(portion.indicesBaseIndex, portion.numIndices).map(i => i - portion.portionBase)
: indicesBuf.getData());
const sceneModelMatrix = model.matrix;
const origin4 = math.vec4();
origin4.set(origin, 0); origin4[3] = 1;
math.mulMat4v4(sceneModelMatrix, origin4, origin4);
const instanceMatrix = modelMatrixColBufs && math.mat4();
if (instanceMatrix) {
const col0 = modelMatrixColBufs[0].getData(portionId, 1);
const col1 = modelMatrixColBufs[1].getData(portionId, 1);
const col2 = modelMatrixColBufs[2].getData(portionId, 1);
instanceMatrix.set([
col0[0], col1[0], col2[0], 0,
col0[1], col1[1], col2[1], 0,
col0[2], col1[2], col2[2], 0,
col0[3], col1[3], col2[3], 1,
]);
}
const matrix = math.mat4();
math.mulMat4(sceneModelMatrix, instanceMatrix ? math.mulMat4(instanceMatrix, positionsDecodeMatrix, matrix) : positionsDecodeMatrix, matrix);
matrix[12] += origin4[0];
matrix[13] += origin4[1];
matrix[14] += origin4[2];
const positionsQuantized = portion ? positionsBuf.getData(portion.portionBase, portion.portionSize) : positionsBuf.getData();
const positions = math.transformPositions3(
matrix,
positionsQuantized,
new Float64Array(positionsQuantized.length));
return { indices, positions };
}),
precisionRayPickSurface: (portionId, worldRayOrigin, worldRayDir, worldSurfacePos, worldNormal) => {
const retainedGeometry = portions[portionId].retainedGeometry;
if (! retainedGeometry) {
return false;
} else {
if (retainedGeometry.matrix && (! retainedGeometry.inverseMatrix)) {
retainedGeometry.inverseMatrix = math.inverseMat4(retainedGeometry.matrix, math.mat4());
}
if (worldNormal && retainedGeometry.inverseMatrix && (! retainedGeometry.normalMatrix)) {
retainedGeometry.normalMatrix = math.transposeMat4(retainedGeometry.inverseMatrix, math.mat4());
}
const positions = retainedGeometry.quantizedPositions;
const indices = retainedGeometry.indices;
const offset = retainedGeometry.offset;
const rtcRayOrigin = tempVec3a;
const rtcRayDir = tempVec3b;
rtcRayOrigin.set(origin ? math.subVec3(worldRayOrigin, origin, tempVec3c) : worldRayOrigin); // World -> RTC
rtcRayDir.set(worldRayDir);
if (offset) {
math.subVec3(rtcRayOrigin, offset);
}
math.transformRay(model.worldNormalMatrix, rtcRayOrigin, rtcRayDir, rtcRayOrigin, rtcRayDir); // RTC -> local
if (retainedGeometry.inverseMatrix) {
math.transformRay(retainedGeometry.inverseMatrix, rtcRayOrigin, rtcRayDir, rtcRayOrigin, rtcRayDir);
}
const a = tempVec3d;
const b = tempVec3e;
const c = tempVec3f;
let gotIntersect = false;
let closestDist = 0;
const closestIntersectPos = tempVec3g;
for (let i = 0, len = indices.length; i < len; i += 3) {
const ia = indices[i] * 3;
const ib = indices[i + 1] * 3;
const ic = indices[i + 2] * 3;
a[0] = positions[ia];
a[1] = positions[ia + 1];
a[2] = positions[ia + 2];
b[0] = positions[ib];
b[1] = positions[ib + 1];
b[2] = positions[ib + 2];
c[0] = positions[ic];
c[1] = positions[ic + 1];
c[2] = positions[ic + 2];
math.decompressPosition(a, positionsDecodeMatrix);
math.decompressPosition(b, positionsDecodeMatrix);
math.decompressPosition(c, positionsDecodeMatrix);
if (math.rayTriangleIntersect(rtcRayOrigin, rtcRayDir, a, b, c, closestIntersectPos)) {
if (retainedGeometry.matrix) {
math.transformPoint3(retainedGeometry.matrix, closestIntersectPos, closestIntersectPos);
}
math.transformPoint3(model.worldMatrix, closestIntersectPos, closestIntersectPos);
if (offset) {
math.addVec3(closestIntersectPos, offset);
}
if (origin) {
math.addVec3(closestIntersectPos, origin);
}
const dist = Math.abs(math.lenVec3(math.subVec3(closestIntersectPos, worldRayOrigin, [])));
if (!gotIntersect || dist > closestDist) {
closestDist = dist;
worldSurfacePos.set(closestIntersectPos);
if (worldNormal) { // Not that wasteful to eagerly compute - unlikely to hit >2 surfaces on most geometry
math.triangleNormal(a, b, c, worldNormal);
}
gotIntersect = true;
}
}
}
if (gotIntersect && worldNormal) {
if (retainedGeometry.normalMatrix) {
math.transformVec3(retainedGeometry.normalMatrix, worldNormal, worldNormal);
}
math.transformVec3(model.worldNormalMatrix, worldNormal, worldNormal);
math.normalizeVec3(worldNormal);
}
return gotIntersect;
}
},
layerTextureSet: textureSet,
renderers: getRenderers(scene, instancing ? "instancing" : "batching", primitive,
model.saoEnabled,
model.pbrEnabled && uvSetup && textureSet && textureSet.colorTexture && normalsBuf && metallicRoughnessBuf && textureSet.metallicRoughnessTexture,
model.colorTextureEnabled && uvSetup && textureSet && textureSet.colorTexture,
!!normalsBuf,
(programVariables) => makeVBORenderingAttributes(programVariables, instancing && { hasModelNormalMat: !!modelNormalMatrixColBufs }, scene.entityOffsetsEnabled)),
drawCalls: (function() {
const drawCallCache = { };
const makeDrawer = (subGeometry) => (attributesHash, layerTypeInputs, viewState) => {
let offset = 0;
const mat4Size = 4 * 4;
matricesUniformBlockBufferData.set(model.rotationMatrix, 0);
matricesUniformBlockBufferData.set(viewState.viewMatrix, offset += mat4Size);
matricesUniformBlockBufferData.set(viewState.projMatrix, offset += mat4Size);
matricesUniformBlockBufferData.set(positionsDecodeMatrix, offset += mat4Size);
if (layerTypeInputs.needNormal()) {
matricesUniformBlockBufferData.set(model.worldNormalMatrix, offset += mat4Size);
matricesUniformBlockBufferData.set(viewState.viewNormalMatrix, offset += mat4Size);
}
layerTypeInputs.matrices.setInputValue(matricesUniformBlockBufferData);
layerTypeInputs.uvDecodeMatrix.setInputValue && layerTypeInputs.uvDecodeMatrix.setInputValue(uvSetup.mat);
if (! (attributesHash in drawCallCache)) {
drawCallCache[attributesHash] = [ null, null, null ];
}
const inputsCache = drawCallCache[attributesHash];
const cacheKey = subGeometry ? (subGeometry.vertices ? 0 : 1) : 2;
if (! inputsCache[cacheKey]) {
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const setAttr = (a, b) => { if (a && a.setInputValue && b) { a.setInputValue(b); } };
const attrs = layerTypeInputs.attributes;
setAttr(attrs.position, positionsBuf);
setAttr(attrs.normal, normalsBuf);
setAttr(attrs.color, colorsBuf);
setAttr(attrs.pickColor, pickColorsBuf);
setAttr(attrs.uV, uvSetup && uvSetup.buf);
setAttr(attrs.metallicRoughness, metallicRoughnessBuf);
setAttr(attrs.flags, flagsBuf);
setAttr(attrs.offset, offsetsBuf);
attrs.modelMatrixCol && attrs.modelMatrixCol.forEach((a, i) => setAttr(a, modelMatrixColBufs[i]));
attrs.modelNormalMatrixCol && attrs.modelNormalMatrixCol.forEach((a, i) => setAttr(a, modelNormalMatrixColBufs[i]));
const drawer = (function() {
// TODO: Use drawElements count and offset to draw only one entity
const drawPoints = () => {
if (instancing) {
gl.drawArraysInstanced(gl.POINTS, 0, positionsBuf.numItems, portions.length);
} else {
gl.drawArrays(gl.POINTS, 0, positionsBuf.numItems);
}
};
const elementsDrawer = (mode, indicesBuf) => {
indicesBuf.bindIndicesBuffer();
const count = indicesBuf.indicesCount;
const type = indicesBuf.indicesType;
return () => {
if (instancing) {
gl.drawElementsInstanced(mode, count, type, 0, portions.length);
} else {
gl.drawElements(mode, count, type, 0);
}
};
};
if (primitive === "points") {
return drawPoints;
} else if (primitive === "lines") {
if (subGeometry && subGeometry.vertices) {
return drawPoints;
} else {
return elementsDrawer(gl.LINES, indicesBuf);
}
} else { // triangles
if (subGeometry && subGeometry.vertices) {
return drawPoints;
} else if (subGeometry) {
return edgeIndicesBuf ? elementsDrawer(gl.LINES, edgeIndicesBuf) : (() => { });
} else {
return elementsDrawer(gl.TRIANGLES, indicesBuf);
}
}
})();
gl.bindVertexArray(null);
cleanups.push(() => gl.deleteVertexArray(vao));
inputsCache[cacheKey] = () => {
gl.bindVertexArray(vao);
drawer();
gl.bindVertexArray(null);
};
}
inputsCache[cacheKey]();
};
return {
drawVertices: makeDrawer({ vertices: true }),
drawEdges: makeDrawer({ }),
drawSurface: makeDrawer(null)
};
})(),
destroy: () => {
cleanups.forEach(c => c());
cleanups.length = 0;
}
};
}
}
const lazyShaderVariable = function(name) {
const variable = {
toString: () => {
variable.needed = true;
return name;
}
};
return variable;
};
const makeVBORenderingAttributes = function(programVariables, instancing, entityOffsetsEnabled) {
const createAttribute = programVariables.createAttribute;
const attributes = {
position: createAttribute("vec3", "positionA"),
normal: createAttribute("vec3", "normalA"),
color: createAttribute("vec4", "colorA"),
pickColor: createAttribute("vec4", "pickColor"),
uV: createAttribute("vec2", "uvApremul"),
metallicRoughness: createAttribute("vec2", "metallicRoughness"),
flags: createAttribute("float", "flagsA"),
offset: entityOffsetsEnabled && createAttribute("vec3", "offset"),
modelMatrixCol: instancing && iota(3).map(i => createAttribute("vec4", "modelMatrixCol" + i)),
modelNormalMatrixCol: instancing && instancing.hasModelNormalMat && iota(3).map(i => createAttribute("vec4", "modelNormalMatrixCol" + i))
};
const uvA = lazyShaderVariable("aUv");
const viewNormal = lazyShaderVariable("viewNormal");
const worldNormal = lazyShaderVariable("worldNormal");
const uvDecodeMatrix = programVariables.createUniform("mat3", "uvDecodeMatrix");
const needNormal = () => (viewNormal.needed || worldNormal.needed);
const matrices = programVariables.createUniformBlock(
"Matrices",
{
worldMatrix: "mat4",
viewMatrix: "mat4",
projMatrix: "mat4",
positionsDecodeMatrix: "mat4",
worldNormalMatrix: "mat4",
viewNormalMatrix: "mat4"
});
return {
dontCullOnAlphaZero: true,
clippableTest: (function() {
const vClippable = programVariables.createVarying("float", "vClippable", () => `${`((int(${attributes.flags}) >> 16 & 0xF) == 1)`} ? 1.0 : 0.0`); // Using `flat uint` for vClippable causes an instability - see XEOK-385
return () => `${vClippable} != 0.0`;
})(),
geometryParameters: {
attributes: {
color: attributes.color,
flags: iota(4).map(i => ({ toString: () => `(int(${attributes.flags}) >> ${i * 4} & 0xF)` })),
metallicRoughness: attributes.metallicRoughness,
normal: {
view: viewNormal,
world: worldNormal
},
pickColor: attributes.pickColor,
position: {
clip: "gl_Position",
view: "viewPosition",
world: "worldPosition"
},
uv: uvA
},
projMatrix: matrices.projMatrix,
viewMatrix: matrices.viewMatrix
},
appendVertexData: (src) => {
uvA.needed && src.push(`vec2 ${uvA} = (${uvDecodeMatrix} * vec3(${attributes.uV}, 1.0)).xy;`);
const modelMatrixTransposed = attributes.modelMatrixCol && `mat4(${attributes.modelMatrixCol[0]}, ${attributes.modelMatrixCol[1]}, ${attributes.modelMatrixCol[2]}, vec4(0.0,0.0,0.0,1.0))`;
src.push(`vec4 worldPosition = ${matrices.worldMatrix} * (${matrices.positionsDecodeMatrix} * vec4(${attributes.position}, 1.0)${modelMatrixTransposed ? (" * " + modelMatrixTransposed) : ""});`);
attributes.offset && src.push(`worldPosition.xyz = worldPosition.xyz + ${attributes.offset};`);
if (needNormal()) {
const timesModelNormalMatrixT = attributes.modelNormalMatrixCol && `* mat4(${attributes.modelNormalMatrixCol[0]}, ${attributes.modelNormalMatrixCol[1]}, ${attributes.modelNormalMatrixCol[2]}, vec4(0.0,0.0,0.0,1.0))`;
src.push(`vec4 modelNormal = vec4(${programVariables.commonLibrary.octDecode}(${attributes.normal}.xy), 0.0)${timesModelNormalMatrixT || ""};`);
src.push(`vec3 ${worldNormal} = (${matrices.worldNormalMatrix} * modelNormal).xyz;`);
if (viewNormal.needed) {
src.push(`vec3 ${viewNormal} = normalize((${matrices.viewNormalMatrix} * vec4(${worldNormal}, 0.0)).xyz);`);
}
}
},
layerTypeInputs: {
attributes: attributes,
matrices: matrices,
uvDecodeMatrix: uvDecodeMatrix,
needNormal: needNormal
}
};
};