@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
958 lines (837 loc) • 47.5 kB
JavaScript
import {ENTITY_FLAGS} from "../ENTITY_FLAGS.js";
import {getColSilhEdgePickFlags, getRenderers, isPerspectiveMatrix, Layer} from "./Layer.js";
import {math} from "../../math/math.js";
import {Configs} from "../../../Configs.js";
const iota = (n) => { const ret = [ ]; for (let i = 0; i < n; ++i) ret.push(i); return ret; };
const dataTextureRamStats = {
sizeDataColorsAndFlags: 0,
sizeDataInstancesMatrices: 0,
sizeDataPositionDecodeMatrices: 0,
sizeDataTextureOffsets: 0,
sizeDataTexturePositions: 0,
sizeDataTextureIndices: 0,
sizeDataTextureEdgeIndices: 0,
sizeDataTexturePortionIds: 0,
numberOfGeometries: 0,
numberOfPortions: 0,
numberOfLayers: 0,
numberOfTextures: 0,
totalPolygons: 0,
totalPolygons8Bits: 0,
totalPolygons16Bits: 0,
totalPolygons32Bits: 0,
totalEdges: 0,
totalEdges8Bits: 0,
totalEdges16Bits: 0,
totalEdges32Bits: 0,
cannotCreatePortion: {
because10BitsObjectId: 0,
becauseTextureSize: 0,
},
overheadSizeAlignementIndices: 0,
overheadSizeAlignementEdgeIndices: 0,
};
window.printDataTextureRamStats = function () {
console.log(JSON.stringify(dataTextureRamStats, null, 4));
let totalRamSize = 0;
Object.keys(dataTextureRamStats).forEach(key => {
if (key.startsWith("size")) {
totalRamSize += dataTextureRamStats[key];
}
});
console.log(`Total size ${totalRamSize} bytes (${(totalRamSize / 1000 / 1000).toFixed(2)} MB)`);
console.log(`Avg bytes / triangle: ${(totalRamSize / dataTextureRamStats.totalPolygons).toFixed(2)}`);
let percentualRamStats = {};
Object.keys(dataTextureRamStats).forEach(key => {
if (key.startsWith("size")) {
percentualRamStats[key] =
`${(dataTextureRamStats[key] / totalRamSize * 100).toFixed(2)} % of total`;
}
});
console.log(JSON.stringify({percentualRamUsage: percentualRamStats}, null, 4));
};
const configs = new Configs();
/**
* 12-bits allowed for object ids.
* Limits the per-object texture height in the layer.
*/
const MAX_NUMBER_OF_OBJECTS_IN_LAYER = (1 << 16);
/**
* 4096 is max data texture height.
* Limits the aggregated geometry texture height in the layer.
*/
const MAX_DATA_TEXTURE_HEIGHT = configs.maxDataTextureHeight;
/**
* Align `indices` and `edgeIndices` memory layout to 8 elements.
*
* Used as an optimization for the `...portionIds...` texture, so it
* can just be stored 1 out of 8 `portionIds` corresponding to a given
* `triangle-index` or `edge-index`.
*/
const INDICES_EDGE_INDICES_ALIGNEMENT_SIZE = 8;
/**
* Number of maximum allowed per-object flags update per render frame
* before switching to batch update mode.
*/
const MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE = 10;
const tempVec3 = math.vec3();
const tempVec3a = math.vec3();
const tempVec4a = math.vec4();
const tempVec4b = math.vec4();
const tempMat4a = new Float32Array(16);
const tempUint8Array4 = new Uint8Array(4);
const tempFloat32Array3 = new Float32Array(3);
const DEFAULT_MATRIX = math.identityMat4();
/**
* @private
*/
export class DTXLayer extends Layer {
constructor(model, primitive, origin) {
super(model, primitive, origin);
dataTextureRamStats.numberOfLayers++;
this._sortId = `TriDTX-${dataTextureRamStats.numberOfLayers}`;
const gl = model.scene.canvas.gl;
const geometryData = (indicesStatsProp, edgesStatsProp) => {
const indicesBuffer = [ ];
let lenIndices = 0;
const perTriangleNumberPortionId = [ ];
let numIndices = 0;
const edgeIndicesBuffer = [ ];
let lenEdgeIndices = 0;
const perEdgeNumberPortionId = [ ];
let numEdgeIndices = 0;
return {
len: () => numIndices,
accumulateIndices: (indices, edgeIndices) => {
const accIndicesSubPortion = (indices && (indices.length > 0)) && (function() {
const indicesBase = lenIndices / 3;
lenIndices += indices.length;
indicesBuffer.push(indices);
const numTriangles = indices.length / 3;
return (subPortionId) => {
const currentNumIndices = numIndices;
numIndices += numTriangles * 3;
buffer.perObjectIndexBaseOffsets.push(currentNumIndices / 3 - indicesBase);
for (let i = 0; i < numTriangles; i += INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) {
perTriangleNumberPortionId.push(subPortionId);
}
dataTextureRamStats[indicesStatsProp] += numTriangles;
dataTextureRamStats.totalPolygons += numTriangles;
};
})();
const accEdgesSubPortion = edgeIndices && (function() {
const edgeIndicesBase = lenEdgeIndices / 2;
lenEdgeIndices += edgeIndices.length;
edgeIndicesBuffer.push(edgeIndices);
const numEdges = edgeIndices.length / 2;
return subPortionId => {
const currentNumEdgeIndices = numEdgeIndices;
numEdgeIndices += numEdges * 2;
buffer.perObjectEdgeIndexBaseOffsets.push(currentNumEdgeIndices / 2 - edgeIndicesBase);
for (let i = 0; i < numEdges; i += INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) {
perEdgeNumberPortionId.push(subPortionId);
}
dataTextureRamStats[edgesStatsProp] += numEdges;
dataTextureRamStats.totalEdges += numEdges;
};
})();
dataTextureRamStats.numberOfGeometries++;
return {
numTriangles: indices ? (indices.length / 3) : 0,
accumulateSubPortionId: subPortionId => {
accIndicesSubPortion && accIndicesSubPortion(subPortionId);
accEdgesSubPortion && accEdgesSubPortion(subPortionId);
}
};
},
createDrawers: (createTextureForSingleItems, indicesType) => {
const createTextureForPackedPortionIds = function(portionIdsArray, defaultIfEmpty) {
return (portionIdsArray.length > 0) ? createTextureForSingleItems([ portionIdsArray ], portionIdsArray.length, 1, gl.UNSIGNED_SHORT, "sizeDataTexturePortionIds") : defaultIfEmpty;
};
// Texture that holds the PortionId that corresponds to a given polygon-id.
const portionIdsTexture = createTextureForPackedPortionIds(perTriangleNumberPortionId, { texture: null });
// Texture that holds the unique-vertex-indices.
const indicesTexture = (lenIndices > 0) && createTextureForSingleItems(indicesBuffer, lenIndices, 3, indicesType, "sizeDataTextureIndices");
// Texture that holds the PortionId that corresponds to a given edge-id.
const portionEdgeIdsTexture = createTextureForPackedPortionIds(perEdgeNumberPortionId);
// Texture that holds the unique-vertex-indices for 8-bit based edge indices.
const edgeIndicesTexture = (lenEdgeIndices > 0) && createTextureForSingleItems(edgeIndicesBuffer, lenEdgeIndices, 2, indicesType, "sizeDataTextureEdgeIndices");
return {
indices: function(layerTypeInputs, glMode) {
if (numIndices > 0) {
layerTypeInputs.perPrimIndices.setInputValue(indicesTexture);
layerTypeInputs.perPrimIdPorIds.setInputValue(portionIdsTexture);
gl.drawArrays(glMode, 0, numIndices);
}
},
edges: function(layerTypeInputs, glMode) {
if (numEdgeIndices > 0) {
layerTypeInputs.perPrimIndices.setInputValue(edgeIndicesTexture);
layerTypeInputs.perPrimIdPorIds.setInputValue(portionEdgeIdsTexture);
gl.drawArrays(glMode, 0, numEdgeIndices);
}
}
};
},
_clearToOptimizeGC: () => { indicesBuffer.length = edgeIndicesBuffer.length = 0; }
};
};
const buffer = this._buffer = {
positionsCompressed: [],
lenPositionsCompressed: 0,
geometry8Bits: geometryData("totalPolygons8Bits", "totalEdges8Bits"),
geometry16Bits: geometryData("totalPolygons16Bits", "totalEdges16Bits"),
geometry32Bits: geometryData("totalPolygons32Bits", "totalEdges32Bits"),
perObjectColors: [],
perObjectPickColors: [],
perObjectSolid: [],
perObjectPositionsDecodeMatrices: [],
perObjectInstancePositioningMatrices: [],
perObjectVertexBases: [],
perObjectIndexBaseOffsets: [],
perObjectEdgeIndexBaseOffsets: []
};
this._numVertices = 0;
this._portions = []; // These counts are used to avoid unnecessary render passes
this._subPortionReadableGeometries = this.model.scene.readableGeometryEnabled && {};
/**
* Due to `index rebucketting` process in ```prepareMeshGeometry``` function, it's possible that a single
* portion is expanded to more than 1 real sub-portion.
*
* This Array tracks the mapping between:
*
* - external `portionIds` as seen by consumers of this class.
* - internal `sub-portionIds` actually managed by this class.
*
* The outer index of this array is the externally seen `portionId`.
* The inner value of the array, are `sub-portionIds` corresponding to the `portionId`.
*/
this._portionToSubPortionsMap = [];
this._bucketGeometries = {};
this._primitive = primitive;
}
/**
* Returns whether the ```TrianglesDataTextureLayer``` has room for more portions.
*
* @param {object} portionCfg An object containing the geometrical data (`positions`, `indices`, `edgeIndices`) for the portion.
* @returns {Boolean} Wheter the requested portion can be created
*/
canCreatePortion(portionCfg) {
const numNewPortions = portionCfg.buckets.length;
if ((this._portions.length + numNewPortions) > MAX_NUMBER_OF_OBJECTS_IN_LAYER) {
dataTextureRamStats.cannotCreatePortion.because10BitsObjectId++;
}
let retVal = (this._portions.length + numNewPortions) <= MAX_NUMBER_OF_OBJECTS_IN_LAYER;
const bucketIndex = 0; // TODO: Is this a bug?
const bucketGeometryId = portionCfg.geometryId !== undefined && portionCfg.geometryId !== null
? `${portionCfg.geometryId}#${bucketIndex}`
: `${portionCfg.id}#${bucketIndex}`;
const alreadyHasPortionGeometry = this._bucketGeometries[bucketGeometryId];
if (!alreadyHasPortionGeometry) {
const buffer = this._buffer;
const maxIndicesOfAnyBits = Math.max(buffer.geometry8Bits.len(), buffer.geometry16Bits.len(), buffer.geometry32Bits.len());
let numVertices = 0;
let numIndices = 0;
portionCfg.buckets.forEach(bucket => {
numVertices += bucket.positionsCompressed.length / 3;
numIndices += bucket.indices.length / 3;
});
if ((this._numVertices + numVertices) > MAX_DATA_TEXTURE_HEIGHT * 4096 ||
(maxIndicesOfAnyBits + numIndices) > MAX_DATA_TEXTURE_HEIGHT * 4096) {
dataTextureRamStats.cannotCreatePortion.becauseTextureSize++;
}
retVal &&=
(this._numVertices + numVertices) <= MAX_DATA_TEXTURE_HEIGHT * 4096 &&
(maxIndicesOfAnyBits + numIndices) <= MAX_DATA_TEXTURE_HEIGHT * 4096;
}
return retVal;
}
/**
* Creates a new portion within this TrianglesDataTextureLayer, returns the new portion ID.
*
* Gives the portion the specified geometry, color and matrix.
*
* @param mesh The SceneModelMesh that owns the portion
* @param portionCfg.positionsCompressed Flat float Local-space positionsCompressed array.
* @param [portionCfg.normals] Flat float normals array.
* @param [portionCfg.colors] Flat float colors array.
* @param portionCfg.indices Flat int indices array.
* @param [portionCfg.edgeIndices] Flat int edges indices array.
* @param portionCfg.color Quantized RGB color [0..255,0..255,0..255,0..255]
* @param portionCfg.opacity Opacity [0..255]
* @param [portionCfg.meshMatrix] Flat float 4x4 matrix - transforms the portion within the coordinate system that's local to the SceneModel
* @param portionCfg.worldAABB Flat float AABB World-space AABB
* @param portionCfg.pickColor Quantized pick color
* @returns {number} Portion ID
*/
createPortion(mesh, portionCfg) {
const buffer = this._buffer;
// const portionAABB = portionCfg.worldAABB;
const subPortionIds = portionCfg.buckets.map((bucket, bucketIndex) => {
const bucketGeometryId = (portionCfg.geometryId ?? portionCfg.id) + "#" + bucketIndex;
// const subPortionAABB = math.collapseAABB3(tempAABB3b);
if (! (bucketGeometryId in this._bucketGeometries)) {
const aligned = (indices, elementSize, statsProp) => {
// Indices and EdgeIndices alignement
// This will make every mesh consume a multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE
// array items for storing the triangles and edges of the mesh, and it supports:
// - a memory optimization of factor INDICES_EDGE_INDICES_ALIGNEMENT_SIZE
// - in exchange for a small RAM overhead
// (by adding some padding until a size that is multiple of INDICES_EDGE_INDICES_ALIGNEMENT_SIZE)
if (indices) {
const alignedLen = Math.ceil((indices.length / elementSize) / INDICES_EDGE_INDICES_ALIGNEMENT_SIZE) * INDICES_EDGE_INDICES_ALIGNEMENT_SIZE * elementSize;
const alignedArray = new Uint32Array(alignedLen);
alignedArray.set(indices);
dataTextureRamStats[statsProp] += 2 * (alignedArray.length - indices.length);
return alignedArray;
} else {
return indices;
}
};
bucket.indices = aligned(bucket.indices, 3, "overheadSizeAlignementIndices");
bucket.edgeIndices = aligned(bucket.edgeIndices, 2, "overheadSizeAlignementEdgeIndices");
const positionsCompressed = bucket.positionsCompressed;
buffer.positionsCompressed.push(positionsCompressed);
const numVertices = positionsCompressed.length / 3;
this._numVertices += numVertices;
const vertexBase = buffer.lenPositionsCompressed / 3;
buffer.lenPositionsCompressed += positionsCompressed.length;
this._bucketGeometries[bucketGeometryId] = {
vertexBase: vertexBase,
geometryData: (function() {
const indices = bucket.indices;
const edgeIndices = bucket.edgeIndices;
if (numVertices <= (1 << 8)) {
return buffer.geometry8Bits.accumulateIndices( indices, edgeIndices);
} else if (numVertices <= (1 << 16)) {
return buffer.geometry16Bits.accumulateIndices(indices, edgeIndices);
} else {
return buffer.geometry32Bits.accumulateIndices(indices, edgeIndices);
}
})()
};
}
//math.expandAABB3(portionAABB, subPortionAABB);
const bucketGeometry = this._bucketGeometries[bucketGeometryId];
buffer.perObjectPositionsDecodeMatrices.push(portionCfg.positionsDecodeMatrix);
buffer.perObjectInstancePositioningMatrices.push(portionCfg.meshMatrix || DEFAULT_MATRIX);
buffer.perObjectSolid.push(!!portionCfg.solid);
buffer.perObjectPickColors.push(portionCfg.pickColor);
buffer.perObjectVertexBases.push(bucketGeometry.vertexBase);
const colors = portionCfg.colors;
const color = portionCfg.color; // Color is pre-quantized by SceneModel
buffer.perObjectColors.push(colors
? [ colors[0] * 255, colors[1] * 255, colors[2] * 255, 255 ]
: [ color[0], color[1], color[2], portionCfg.opacity ]);
const subPortionId = this._portions.length;
this._portions.push({ });
bucketGeometry.geometryData.accumulateSubPortionId(subPortionId);
if (this._subPortionReadableGeometries) {
this._subPortionReadableGeometries[subPortionId] = {
indices: bucket.indices,
positionsCompressed: bucket.positionsCompressed,
positionsDecodeMatrix: portionCfg.positionsDecodeMatrix
};
}
dataTextureRamStats.numberOfPortions++;
return subPortionId;
});
const portionId = this._portionToSubPortionsMap.length;
this._portionToSubPortionsMap.push(subPortionIds);
this.model.numPortions++;
this._meshes.push(mesh);
return portionId;
}
/**
* Builds data textures from the appended geometries and loads them into the GPU.
*
* No more portions can then be created.
*/
compilePortions() {
const buffer = this._buffer;
const model = this.model;
const numPortions = this._portions.length;
const origin = this.origin;
const portionToSubPortionsMap = this._portionToSubPortionsMap;
const primitive = this._primitive;
const sortId = this._sortId;
const subPortionReadableGeometries = this._subPortionReadableGeometries;
const scene = model.scene;
const gl = scene.canvas.gl;
/*
* Texture that holds colors/pickColors/flags/flags2 per-object:
* - columns: one concept per column => color / pick-color / ...
* - row: the object Id
* The texture will have:
* - 4 RGBA columns per row: for each object (pick) color and flags(2)
* - N rows where N is the number of objects
*/
const populateTexArray = texArray => {
const pack32as4x8 = ui32 => [
(ui32 >> 24) & 255,
(ui32 >> 16) & 255,
(ui32 >> 8) & 255,
(ui32) & 255
];
for (let i = 0; i < numPortions; i++) {
// 8 columns per texture row:
texArray.set(buffer.perObjectColors[i], i * 32 + 0); // (RGBA)
texArray.set(buffer.perObjectPickColors[i], i * 32 + 4); // (packed Uint32 as RGBA)
texArray.set([0, 0, 0, 0], /* flags */ i * 32 + 8); // (packed 4 bytes as RGBA)
texArray.set([0, 0, 0, 0], /* flags2 */ i * 32 + 12); // (packed 4 bytes as RGBA)
texArray.set(pack32as4x8(buffer.perObjectVertexBases[i]), i * 32 + 16); // (packed Uint32 bytes as RGBA)
texArray.set(pack32as4x8(buffer.perObjectIndexBaseOffsets[i]), i * 32 + 20); // (packed Uint32 bytes as RGBA)
texArray.set(pack32as4x8(buffer.perObjectEdgeIndexBaseOffsets[i]), i * 32 + 24); // (packed Uint32 bytes as RGBA)
texArray.set([buffer.perObjectSolid[i] ? 1 : 0, 0, 0, 0], i * 32 + 28); // (packed 4 bytes as RGBA)
}
};
// The number of rows in the texture is the number of objects in the layer.
const texturePerObjectColorsAndFlags = createBindableDataTexture(gl, numPortions, 32, gl.UNSIGNED_BYTE, 512, populateTexArray, "sizeDataColorsAndFlags", true);
const texturePerObjectColorsAndFlagsData = texturePerObjectColorsAndFlags.textureData;
const createDataTexture = function(dataArrays, entitiesCnt, entitySize, type, entitiesPerRow, statsProp, exposeData) {
const populateTexArray = texArray => {
for (let i = 0, j = 0, len = dataArrays.length; i < len; i++) {
const pc = dataArrays[i];
texArray.set(pc, j);
j += pc.length;
}
};
return createBindableDataTexture(gl, entitiesCnt, entitySize, type, entitiesPerRow, populateTexArray, statsProp, exposeData);
};
const createTextureForMatrices = function(matrices, statsProp, exposeData) {
const numMatrices = matrices.length;
if (numMatrices === 0) {
throw "num " + statsProp + " matrices===0";
}
// in one row we can fit 512 matrices
return createDataTexture(matrices, numMatrices, 16, gl.FLOAT, 512, statsProp, exposeData);
};
/**
* This will generate a texture for all positions decode matrices in the layer.
* The texture will have:
* - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components).
* Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix)
* - N rows where N is the number of objects
*/
const texturePerObjectInstanceMatrices = createTextureForMatrices(buffer.perObjectInstancePositioningMatrices, "sizeDataInstancesMatrices", true);
const texturePerObjectInstanceMatricesData = texturePerObjectInstanceMatrices.textureData;
/*
* Texture that holds the objectDecodeAndInstanceMatrix per-object:
* - columns: each column is one column of the matrix
* - row: the object Id
* The texture will have:
* - 4 RGBA columns per row (each column will contain 4 packed half-float (16 bits) components).
* Thus, each row will contain 16 packed half-floats corresponding to a complete positions decode matrix)
* - N rows where N is the number of objects
*/
const texturePerObjectPositionsDecodeMatrix = createTextureForMatrices(buffer.perObjectPositionsDecodeMatrices, "sizeDataPositionDecodeMatrices", false);
const createTextureForSingleItems = function(dataArrays, dataCnt, entitySize, type, statsProp) {
return createDataTexture(dataArrays, dataCnt / entitySize, entitySize, type, 4096, statsProp, false);
};
/*
* Texture that holds all the `different-vertices` used by the layer.
* This will generate a texture for positions in the layer.
*
* The texture will have:
* - 1024 columns, where each pixel will be a 16-bit-per-component RGB texture, corresponding to the XYZ of the position
* - a number of rows R where R*1024 is just >= than the number of vertices (positions / 3)
*/
const texturePerVertexIdCoordinates = createTextureForSingleItems(
buffer.positionsCompressed, buffer.lenPositionsCompressed, 3, gl.UNSIGNED_SHORT, "sizeDataTexturePositions");
const draw8 = buffer.geometry8Bits.createDrawers( createTextureForSingleItems, gl.UNSIGNED_BYTE);
const draw16 = buffer.geometry16Bits.createDrawers(createTextureForSingleItems, gl.UNSIGNED_SHORT);
const draw32 = buffer.geometry32Bits.createDrawers(createTextureForSingleItems, gl.UNSIGNED_INT);
// Optimization to free up memory (XCD-408 and XCD-424).
// The following lines were added to assist GC in cleaning up some of the data. See also the `texArray = null`.
// A Chrome test with Lyon[1-9].xkt models loaded simultaneously showed a decrease of 596MB to 158MB.
this._bucketGeometries = null;
this._buffer.geometry8Bits._clearToOptimizeGC();
this._buffer.geometry16Bits._clearToOptimizeGC();
this._buffer.geometry32Bits._clearToOptimizeGC();
Object.keys(this._buffer).forEach(k => this._buffer[k] = null);
this._buffer = null;
let deferredSetFlagsActive = false;
let deferredSetFlagsDirty = false;
let destroyed = false;
let numUpdatesInFrame = 0;
/**
* This will _start_ a "set-flags transaction".
*
* After invoking this method, calling setFlags/setFlags2 will not update
* the colors+flags texture but only store the new flags/flag2 in the
* colors+flags texture data array.
*
* After invoking this method, and when all desired setFlags/setFlags2 have
* been called on needed portions of the layer, invoke `uploadDeferredFlags`
* to actually upload the data array into the texture.
*
* In massive "set-flags" scenarios like VFC or LOD mechanisms, the combination of
* `beginDeferredFlags` + `uploadDeferredFlags`brings a speed-up of
* up to 80x when e.g. objects are massively (un)culled 🚀.
*/
const beginDeferredFlags = () => { deferredSetFlagsActive = true; };
const onSceneRendering = scene.on("rendering", () => {
if (deferredSetFlagsDirty) {
// uploadDeferredFlags
/**
* This will _commit_ a "set-flags transaction".
* Invoking this method will update the colors+flags texture data with new
* flags/flags2 set since the previous invocation of `beginDeferredFlags`.
*/
deferredSetFlagsActive = false;
deferredSetFlagsDirty = false;
texturePerObjectColorsAndFlagsData.reloadData();
}
numUpdatesInFrame = 0;
});
const forEachSubPortionId = (portionId, cb) => {
const subPortionIds = portionToSubPortionsMap[portionId];
if (!subPortionIds) {
model.error("portion not found: " + portionId);
} else {
for (let i = 0, len = subPortionIds.length; i < len; i++) {
cb(subPortionIds[i]);
}
}
};
const setPortionColorsAndFlags = (subPortionId, offset, data, deferred) => {
const defer = deferredSetFlagsActive || deferred;
if (defer) {
deferredSetFlagsDirty = true;
} else if (++numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) {
beginDeferredFlags(); // Subsequent flags updates now deferred
}
texturePerObjectColorsAndFlagsData.setData(data, subPortionId, offset, !defer);
};
const setFlags2 = (portionId, flags, deferred = false) => {
tempUint8Array4.set([ (flags & ENTITY_FLAGS.CLIPPABLE) ? 255 : 0, 0, 1, 2 ]);
forEachSubPortionId(portionId, subPortionId => setPortionColorsAndFlags(subPortionId, 3, tempUint8Array4, deferred));
};
return {
edgesColorOpaqueAllowed: () => {
if (scene.logarithmicDepthBufferEnabled) {
if (!scene._loggedWarning) {
console.log("Edge enhancement for SceneModel data texture layers currently disabled with logarithmic depth buffer");
scene._loggedWarning = true;
}
return false;
} else {
return true;
}
},
sortId: sortId,
setClippableFlags: setFlags2,
setFlags: (portionId, flags, transparent, deferred = false) => {
getColSilhEdgePickFlags(flags, transparent, true, scene, tempUint8Array4);
forEachSubPortionId(portionId, subPortionId => setPortionColorsAndFlags(subPortionId, 2, tempUint8Array4, deferred));
},
setFlags2: setFlags2,
setDeferredFlags: () => { },
setColor: (portionId, color) => {
tempUint8Array4.set(color);
forEachSubPortionId(portionId, subPortionId => setPortionColorsAndFlags(subPortionId, 0, tempUint8Array4, false));
},
setMatrix: (portionId, matrix) => {
forEachSubPortionId(portionId, subPortionId => {
const defer = deferredSetFlagsActive;
if (defer) {
deferredSetFlagsDirty = true;
} else if (++numUpdatesInFrame >= MAX_OBJECT_UPDATES_IN_FRAME_WITHOUT_BATCHED_UPDATE) {
beginDeferredFlags(); // Subsequent flags updates now deferred
}
tempMat4a.set(matrix);
texturePerObjectInstanceMatricesData.setData(tempMat4a, subPortionId, 0, !defer);
});
},
setOffset: (portionId, offset) => { /* NOT COMPLETE */ },
getEachIndex: (portionId, callback) => {
if (subPortionReadableGeometries) {
forEachSubPortionId(
portionId,
subPortionId => subPortionReadableGeometries[subPortionId].indices.forEach(i => callback(i)));
}
},
getEachVertex: (portionId, callback) => {
if (subPortionReadableGeometries) {
forEachSubPortionId(portionId, subPortionId => {
const subPortionReadableGeometry = subPortionReadableGeometries[subPortionId];
const positions = subPortionReadableGeometry.positionsCompressed;
const positionsDecodeMatrix = subPortionReadableGeometry.positionsDecodeMatrix;
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];
worldPos[3] = 1.0;
math.decompressPosition(worldPos, positionsDecodeMatrix);
math.mulMat4v4(model.worldMatrix, worldPos, worldPos);
math.addVec3(origin, worldPos, worldPos);
callback(worldPos);
}
});
}
},
precisionRayPickSurface: (portionId, worldRayOrigin, worldRayDir, worldSurfacePos, worldNormal) => false,
renderers: getRenderers(scene, "dtx", primitive, model.saoEnabled, false, false, true,
(programVariables, subGeometry) => makeDTXRenderingAttributes(programVariables, !subGeometry)),
drawCalls: (function() {
const drawer = function(drawCall) {
const setInputValue = (input, value) => (input.setInputValue && input.setInputValue(value));
return (attributesHash, layerTypeInputs, viewState) => {
setInputValue(layerTypeInputs.worldMatrix, model.rotationMatrix);
setInputValue(layerTypeInputs.viewMatrix, viewState.viewMatrix);
setInputValue(layerTypeInputs.projMatrix, viewState.projMatrix);
const rtcOrigin = math.transformPoint3(model.matrix, origin, tempVec3);
setInputValue(layerTypeInputs.uCameraEyeRtc, math.subVec3(viewState.eye, rtcOrigin, tempVec3a));
setInputValue(layerTypeInputs.perObjPosDecode, texturePerObjectPositionsDecodeMatrix);
setInputValue(layerTypeInputs.perVertIdCoords, texturePerVertexIdCoordinates);
setInputValue(layerTypeInputs.perObjColsFlags, texturePerObjectColorsAndFlags);
setInputValue(layerTypeInputs.perObjectMatrix, texturePerObjectInstanceMatrices);
drawCall(layerTypeInputs);
};
};
const vertEdgesDrawer = function(glMode) {
return drawer((layerTypeInputs) => {
draw8.edges( layerTypeInputs, glMode);
draw16.edges(layerTypeInputs, glMode);
draw32.edges(layerTypeInputs, glMode);
});
};
return {
drawVertices: vertEdgesDrawer(gl.POINTS),
drawEdges: vertEdgesDrawer(gl.LINES),
drawSurface: drawer((layerTypeInputs) => {
const glMode = gl.TRIANGLES;
draw8.indices( layerTypeInputs, glMode);
draw16.indices(layerTypeInputs, glMode);
draw32.indices(layerTypeInputs, glMode);
})
};
})(),
destroy: () => {
if (! destroyed) {
scene.off(onSceneRendering);
destroyed = true;
}
}
};
}
}
const createBindableDataTexture = function(gl, entitiesCnt, entitySize, type, entitiesPerRow, populateTexArray, statsProp, exposeData) {
if ((entitySize > 4) && ((entitySize % 4) > 0)) {
throw "Unhandled data size " + entitySize;
}
const pixelsPerEntity = Math.ceil(entitySize / 4);
const pixelWidth = entitySize / pixelsPerEntity;
const [ arrayType, internalFormat, format ] = (function() {
switch(type) {
case gl.UNSIGNED_BYTE:
return [ Uint8Array, ...((pixelWidth === 1) ? [ gl.R8UI, gl.RED_INTEGER ] : ((pixelWidth === 2) ? [ gl.RG8UI, gl.RG_INTEGER ] : ((pixelWidth === 3) ? [ gl.RGB8UI, gl.RGB_INTEGER ] : [ gl.RGBA8UI, gl.RGBA_INTEGER ]))) ];
case gl.UNSIGNED_SHORT:
return [ Uint16Array, ...((pixelWidth === 1) ? [ gl.R16UI, gl.RED_INTEGER ] : ((pixelWidth === 2) ? [ gl.RG16UI, gl.RG_INTEGER ] : ((pixelWidth === 3) ? [ gl.RGB16UI, gl.RGB_INTEGER ] : [ gl.RGBA16UI, gl.RGBA_INTEGER ]))) ];
case gl.UNSIGNED_INT:
return [ Uint32Array, ...((pixelWidth === 1) ? [ gl.R32UI, gl.RED_INTEGER ] : ((pixelWidth === 2) ? [ gl.RG32UI, gl.RG_INTEGER ] : ((pixelWidth === 3) ? [ gl.RGB32UI, gl.RGB_INTEGER ] : [ gl.RGBA32UI, gl.RGBA_INTEGER ]))) ];
case gl.FLOAT:
return [ Float32Array, ...((pixelWidth === 1) ? [ gl.R32F, gl.RED ] : ((pixelWidth === 2) ? [ gl.RG32F, gl.RG ] : ((pixelWidth === 3) ? [ gl.RGB32F, gl.RGB ] : [ gl.RGBA32F, gl.RGBA ]))) ];
default:
throw "Unhandled data type " + type;
}
})();
const textureWidth = entitiesPerRow * pixelsPerEntity;
const textureHeight = Math.ceil(entitiesCnt / entitiesPerRow);
if (textureHeight === 0) {
throw "texture height===0";
}
let texArray = new arrayType(textureWidth * textureHeight * pixelWidth);
dataTextureRamStats[statsProp] += texArray.byteLength;
dataTextureRamStats.numberOfTextures++;
populateTexArray(texArray);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texStorage2D(gl.TEXTURE_2D, 1, internalFormat, textureWidth, textureHeight);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, format, type, texArray);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null);
if (! exposeData) {
texArray = null; // See the comment related to XCD-408 and XCD-424 above.
}
return {
// called by createSampler::bindTexture
bind(unit) {
gl.activeTexture(gl["TEXTURE" + unit]);
gl.bindTexture(gl.TEXTURE_2D, texture);
return true;
},
unbind(unit) {
// This `unbind` method is ignored at the moment to allow avoiding
// to rebind same texture already bound to a texture unit.
// this._gl.activeTexture(gl["TEXTURE" + unit]);
// this._gl.bindTexture(gl.TEXTURE_2D, null);
},
textureData: exposeData && {
setData(data, subPortionId, offset = 0, load = false) {
texArray.set(data, subPortionId * entitySize + offset * pixelWidth);
if (load) {
gl.bindTexture(gl.TEXTURE_2D, texture);
const xoffset = (subPortionId % entitiesPerRow) * pixelsPerEntity + offset;
const yoffset = Math.floor(subPortionId / entitiesPerRow);
const width = data.length / pixelWidth;
gl.texSubImage2D(gl.TEXTURE_2D, 0, xoffset, yoffset, width, 1, format, type, data);
// gl.bindTexture (gl.TEXTURE_2D, null);
}
},
reloadData() {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, textureWidth, textureHeight, format, type, texArray);
}
}
};
};
const makeDTXRenderingAttributes = function(programVariables, isTriangle) {
const setupTex = (type, name) => {
const map = programVariables.createUniform(type, name);
const texelFetch = (P) => `texelFetch(${map}, ${P}, 0)`;
texelFetch.map = map;
return texelFetch;
};
const perPrimIndices = setupTex("highp usampler2D", "perPrimIndices");
const perPrimIdPorIds = setupTex("mediump usampler2D", "perPrimIdPorIds");
const perObjPosDecode = setupTex("highp sampler2D", "perObjPosDecode");
const perVertIdCoords = setupTex("mediump usampler2D", "perVertIdCoords");
const perObjColsFlags = setupTex("lowp usampler2D", "perObjColsFlags");
const perObjectMatrix = setupTex("highp sampler2D", "perObjectMatrix");
const worldMatrix = programVariables.createUniform("mat4", "worldMatrix");
const viewMatrix = programVariables.createUniform("mat4", "viewMatrix");
const projMatrix = programVariables.createUniform("mat4", "projMatrix");
const uCameraEyeRtc = programVariables.createUniform("vec3", "uCameraEyeRtc");
const lazyShaderVariable = function(name) {
const variable = {
toString: () => {
variable.needed = true;
return name;
}
};
return variable;
};
const colorA = lazyShaderVariable("colorA");
const pickColorA = lazyShaderVariable("pickColor");
const worldNormal = lazyShaderVariable("worldNormal");
const viewNormal = lazyShaderVariable("viewNormal");
const colorsAndFlags = (offset) => perObjColsFlags(`ivec2(objectIndexCoords.x*8+${offset}, objectIndexCoords.y)`);
return {
clippableTest: (function() {
const vClippable = programVariables.createVarying("uint", "vClippable", () => "flags2.r", "flat");
return () => `${vClippable} > 0u`;
})(),
geometryParameters: {
attributes: {
color: colorA,
flags: iota(4).map(i => `int(flags[${i}])`),
metallicRoughness: null,
normal: {
view: viewNormal,
world: worldNormal
},
pickColor: pickColorA,
position: {
clip: "gl_Position",
view: "viewPosition",
world: "worldPosition"
},
uv: null
},
projMatrix: projMatrix,
viewMatrix: viewMatrix
},
ensureColorAndFlagAvailable: (src) => {
// constants
src.push("int primitiveIndex = gl_VertexID / " + (isTriangle ? 3 : 2) + ";");
// get packed object-id
src.push("int h_packed_object_id_index = (primitiveIndex >> 3) & 4095;");
src.push("int v_packed_object_id_index = (primitiveIndex >> 3) >> 12;");
src.push(`int objectIndex = int(${perPrimIdPorIds("ivec2(h_packed_object_id_index, v_packed_object_id_index)")}.r);`);
src.push("ivec2 objectIndexCoords = ivec2(objectIndex % 512, objectIndex / 512);");
// get flags & flags2
src.push(`uvec4 flags = ${colorsAndFlags(2)};`);
src.push(`uvec4 flags2 = ${colorsAndFlags(3)};`);
colorA.needed && src.push(`vec4 ${colorA} = vec4(${colorsAndFlags(0)}) / 255.0;`);
},
appendVertexData: (src) => {
const objMatrix = (offset) => perObjectMatrix(`ivec2(objectIndexCoords.x*4+${offset}, objectIndexCoords.y)`);
src.push(`mat4 objectInstanceMatrix = mat4(${objMatrix(0)}, ${objMatrix(1)}, ${objMatrix(2)}, ${objMatrix(3)});`);
const posMatrix = (offset) => perObjPosDecode(`ivec2(objectIndexCoords.x*4+${offset}, objectIndexCoords.y)`);
src.push(`mat4 objectDecodeAndInstanceMatrix = objectInstanceMatrix * mat4(${posMatrix(0)}, ${posMatrix(1)}, ${posMatrix(2)}, ${posMatrix(3)});`);
src.push(`ivec4 packedVertexBase = ivec4(${colorsAndFlags(4)});`);
src.push(`ivec4 packedIndexBaseOffset = ivec4(${colorsAndFlags(isTriangle ? 5 : 6)});`);
src.push("int indexBaseOffset = (packedIndexBaseOffset.r << 24) + (packedIndexBaseOffset.g << 16) + (packedIndexBaseOffset.b << 8) + packedIndexBaseOffset.a;");
src.push("int h_index = (primitiveIndex - indexBaseOffset) & 4095;");
src.push("int v_index = (primitiveIndex - indexBaseOffset) >> 12;");
src.push(`ivec3 vertexIndices = ivec3(${perPrimIndices("ivec2(h_index, v_index)")});`);
src.push("ivec3 uniqueVertexIndexes = vertexIndices + (packedVertexBase.r << 24) + (packedVertexBase.g << 16) + (packedVertexBase.b << 8) + packedVertexBase.a;");
if (isTriangle) {
src.push("ivec3 indexPositionH = uniqueVertexIndexes & 4095;");
src.push("ivec3 indexPositionV = uniqueVertexIndexes >> 12;");
src.push(`uint solid = ${colorsAndFlags(7)}.r;`);
const vertIdCoords = (idx) => `vec3(${perVertIdCoords(`ivec2(indexPositionH[${idx}], indexPositionV[${idx}])`)})`;
src.push(`vec3 positions[] = vec3[](${vertIdCoords(0)}, ${vertIdCoords(1)}, ${vertIdCoords(2)});`);
src.push("vec3 normal = normalize(cross(positions[2] - positions[0], positions[1] - positions[0]));");
src.push("vec3 position = positions[gl_VertexID % 3];");
if (worldNormal.needed) { // WARNING: Not thoroughly tested, as not being used at the moment
src.push(`vec3 worldNormal = -normalize((transpose(inverse(${worldMatrix} * objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);`);
}
if (viewNormal.needed) {
src.push(`vec3 viewNormal = -normalize((transpose(inverse(${viewMatrix} * objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);`);
}
// when the geometry is not solid, if needed, flip the triangle winding
src.push("if (solid != 1u) {");
src.push(` if (${isPerspectiveMatrix(projMatrix)}) {`);
src.push(` vec3 uCameraEyeRtcInQuantizedSpace = (inverse(${worldMatrix} * objectDecodeAndInstanceMatrix) * vec4(${uCameraEyeRtc}, 1)).xyz;`);
src.push(" if (dot(position.xyz - uCameraEyeRtcInQuantizedSpace, normal) < 0.0) {");
src.push(" position = positions[2 - (gl_VertexID % 3)];");
if (worldNormal.needed) {
src.push(" worldNormal = -worldNormal;");
}
if (viewNormal.needed) {
src.push(" viewNormal = -viewNormal;");
}
src.push(" }");
src.push(" } else {");
if (!viewNormal.needed) {
src.push(` vec3 viewNormal = -normalize((transpose(inverse(${viewMatrix} * objectDecodeAndInstanceMatrix)) * vec4(normal,1)).xyz);`);
}
src.push(" if (viewNormal.z < 0.0) {");
src.push(" position = positions[2 - (gl_VertexID % 3)];");
if (worldNormal.needed) {
src.push(" worldNormal = -worldNormal;");
}
if (viewNormal.needed) {
src.push(" viewNormal = -viewNormal;");
}
src.push(" }");
src.push(" }");
src.push("}");
} else {
src.push("int indexPositionH = uniqueVertexIndexes[gl_VertexID % 2] & 4095;");
src.push("int indexPositionV = uniqueVertexIndexes[gl_VertexID % 2] >> 12;");
src.push(`vec3 position = vec3(${perVertIdCoords("ivec2(indexPositionH, indexPositionV)")});`);
}
pickColorA.needed && src.push(`vec4 pickColor = vec4(${colorsAndFlags(1)});`); // TODO: Normalize color "/ 255.0"?
src.push(`vec4 worldPosition = ${worldMatrix} * (objectDecodeAndInstanceMatrix * vec4(position, 1.0));`);
},
layerTypeInputs: {
perPrimIndices: perPrimIndices.map,
perPrimIdPorIds: perPrimIdPorIds.map,
perObjPosDecode: perObjPosDecode.map,
perVertIdCoords: perVertIdCoords.map,
perObjColsFlags: perObjColsFlags.map,
perObjectMatrix: perObjectMatrix.map,
worldMatrix: worldMatrix,
viewMatrix: viewMatrix,
projMatrix: projMatrix,
uCameraEyeRtc: uCameraEyeRtc
}
};
};