src/XKTModel/XKTModel.js
import {math} from "../lib/math.js";
import {geometryCompression} from "./lib/geometryCompression.js";
import {buildEdgeIndices} from "./lib/buildEdgeIndices.js";
import {isTriangleMeshSolid} from "./lib/isTriangleMeshSolid.js";
import {XKTMesh} from './XKTMesh.js';
import {XKTGeometry} from './XKTGeometry.js';
import {XKTEntity} from './XKTEntity.js';
import {XKTTile} from './XKTTile.js';
import {KDNode} from "./KDNode.js";
import {XKTMetaObject} from "./XKTMetaObject.js";
import {XKTPropertySet} from "./XKTPropertySet.js";
import {mergeVertices} from "../lib/mergeVertices.js";
import {XKT_INFO} from "../XKT_INFO.js";
import {XKTTexture} from "./XKTTexture";
import {XKTTextureSet} from "./XKTTextureSet";
import {encode, load} from "@loaders.gl/core";
import {KTX2BasisWriter} from "@loaders.gl/textures";
import {ImageLoader} from '@loaders.gl/images';
const tempVec4a = math.vec4([0, 0, 0, 1]);
const tempVec4b = math.vec4([0, 0, 0, 1]);
const tempMat4 = math.mat4();
const tempMat4b = math.mat4();
const kdTreeDimLength = new Float64Array(3);
// XKT texture types
const COLOR_TEXTURE = 0;
const METALLIC_ROUGHNESS_TEXTURE = 1;
const NORMALS_TEXTURE = 2;
const EMISSIVE_TEXTURE = 3;
const OCCLUSION_TEXTURE = 4;
// KTX2 encoding options for each texture type
const TEXTURE_ENCODING_OPTIONS = {}
TEXTURE_ENCODING_OPTIONS[COLOR_TEXTURE] = {
useSRGB: true,
qualityLevel: 50,
encodeUASTC: true,
mipmaps: true
};
TEXTURE_ENCODING_OPTIONS[EMISSIVE_TEXTURE] = {
useSRGB: true,
encodeUASTC: true,
qualityLevel: 10,
mipmaps: false
};
TEXTURE_ENCODING_OPTIONS[METALLIC_ROUGHNESS_TEXTURE] = {
useSRGB: false,
encodeUASTC: true,
qualityLevel: 50,
mipmaps: true // Needed for GGX roughness shading
};
TEXTURE_ENCODING_OPTIONS[NORMALS_TEXTURE] = {
useSRGB: false,
encodeUASTC: true,
qualityLevel: 10,
mipmaps: false
};
TEXTURE_ENCODING_OPTIONS[OCCLUSION_TEXTURE] = {
useSRGB: false,
encodeUASTC: true,
qualityLevel: 10,
mipmaps: false
};
/**
* A document model that represents the contents of an .XKT file.
*
* * An XKTModel contains {@link XKTTile}s, which spatially subdivide the model into axis-aligned, box-shaped regions.
* * Each {@link XKTTile} contains {@link XKTEntity}s, which represent the objects within its region.
* * Each {@link XKTEntity} has {@link XKTMesh}s, which each have a {@link XKTGeometry}. Each {@link XKTGeometry} can be shared by multiple {@link XKTMesh}s.
* * Import models into an XKTModel using {@link parseGLTFJSONIntoXKTModel}, {@link parseIFCIntoXKTModel}, {@link parseCityJSONIntoXKTModel} etc.
* * Build an XKTModel programmatically using {@link XKTModel#createGeometry}, {@link XKTModel#createMesh} and {@link XKTModel#createEntity}.
* * Serialize an XKTModel to an ArrayBuffer using {@link writeXKTModelToArrayBuffer}.
*
* ## Usage
*
* See [main docs page](/docs/#javascript-api) for usage examples.
*
* @class XKTModel
*/
class XKTModel {
/**
* Constructs a new XKTModel.
*
* @param {*} [cfg] Configuration
* @param {Number} [cfg.edgeThreshold=10]
* @param {Number} [cfg.minTileSize=500]
*/
constructor(cfg = {}) {
/**
* The model's ID, if available.
*
* Will be "default" by default.
*
* @type {String}
*/
this.modelId = cfg.modelId || "default";
/**
* The project ID, if available.
*
* Will be an empty string by default.
*
* @type {String}
*/
this.projectId = cfg.projectId || "";
/**
* The revision ID, if available.
*
* Will be an empty string by default.
*
* @type {String}
*/
this.revisionId = cfg.revisionId || "";
/**
* The model author, if available.
*
* Will be an empty string by default.
*
* @property author
* @type {String}
*/
this.author = cfg.author || "";
/**
* The date the model was created, if available.
*
* Will be an empty string by default.
*
* @property createdAt
* @type {String}
*/
this.createdAt = cfg.createdAt || "";
/**
* The application that created the model, if available.
*
* Will be an empty string by default.
*
* @property creatingApplication
* @type {String}
*/
this.creatingApplication = cfg.creatingApplication || "";
/**
* The model schema version, if available.
*
* In the case of IFC, this could be "IFC2x3" or "IFC4", for example.
*
* Will be an empty string by default.
*
* @property schema
* @type {String}
*/
this.schema = cfg.schema || "";
/**
* The XKT format version.
*
* @property xktVersion;
* @type {number}
*/
this.xktVersion = XKT_INFO.xktVersion;
/**
*
* @type {Number|number}
*/
this.edgeThreshold = cfg.edgeThreshold || 10;
/**
* Minimum diagonal size of the boundary of an {@link XKTTile}.
*
* @type {Number|number}
*/
this.minTileSize = cfg.minTileSize || 500;
/**
* Optional overall AABB that contains all the {@link XKTEntity}s we'll create in this model, if previously known.
*
* This is the AABB of a complete set of input files that are provided as a split-model set for conversion.
*
* This is used to help the {@link XKTTile.aabb}s within split models align neatly with each other, as we
* build them with a k-d tree in {@link XKTModel#finalize}. Without this, the AABBs of the different parts
* tend to misalign slightly, resulting in excess number of {@link XKTTile}s, which degrades memory and rendering
* performance when the XKT is viewer in the xeokit Viewer.
*/
this.modelAABB = cfg.modelAABB;
/**
* Map of {@link XKTPropertySet}s within this XKTModel, each mapped to {@link XKTPropertySet#propertySetId}.
*
* Created by {@link XKTModel#createPropertySet}.
*
* @type {{String:XKTPropertySet}}
*/
this.propertySets = {};
/**
* {@link XKTPropertySet}s within this XKTModel.
*
* Each XKTPropertySet holds its position in this list in {@link XKTPropertySet#propertySetIndex}.
*
* Created by {@link XKTModel#finalize}.
*
* @type {XKTPropertySet[]}
*/
this.propertySetsList = [];
/**
* Map of {@link XKTMetaObject}s within this XKTModel, each mapped to {@link XKTMetaObject#metaObjectId}.
*
* Created by {@link XKTModel#createMetaObject}.
*
* @type {{String:XKTMetaObject}}
*/
this.metaObjects = {};
/**
* {@link XKTMetaObject}s within this XKTModel.
*
* Each XKTMetaObject holds its position in this list in {@link XKTMetaObject#metaObjectIndex}.
*
* Created by {@link XKTModel#finalize}.
*
* @type {XKTMetaObject[]}
*/
this.metaObjectsList = [];
/**
* The positions of all shared {@link XKTGeometry}s are de-quantized using this singular
* de-quantization matrix.
*
* This de-quantization matrix is generated from the collective Local-space boundary of the
* positions of all shared {@link XKTGeometry}s.
*
* @type {Float32Array}
*/
this.reusedGeometriesDecodeMatrix = new Float32Array(16);
/**
* Map of {@link XKTGeometry}s within this XKTModel, each mapped to {@link XKTGeometry#geometryId}.
*
* Created by {@link XKTModel#createGeometry}.
*
* @type {{Number:XKTGeometry}}
*/
this.geometries = {};
/**
* List of {@link XKTGeometry}s within this XKTModel, in the order they were created.
*
* Each XKTGeometry holds its position in this list in {@link XKTGeometry#geometryIndex}.
*
* Created by {@link XKTModel#finalize}.
*
* @type {XKTGeometry[]}
*/
this.geometriesList = [];
/**
* Map of {@link XKTTexture}s within this XKTModel, each mapped to {@link XKTTexture#textureId}.
*
* Created by {@link XKTModel#createTexture}.
*
* @type {{Number:XKTTexture}}
*/
this.textures = {};
/**
* List of {@link XKTTexture}s within this XKTModel, in the order they were created.
*
* Each XKTTexture holds its position in this list in {@link XKTTexture#textureIndex}.
*
* Created by {@link XKTModel#finalize}.
*
* @type {XKTTexture[]}
*/
this.texturesList = [];
/**
* Map of {@link XKTTextureSet}s within this XKTModel, each mapped to {@link XKTTextureSet#textureSetId}.
*
* Created by {@link XKTModel#createTextureSet}.
*
* @type {{Number:XKTTextureSet}}
*/
this.textureSets = {};
/**
* List of {@link XKTTextureSet}s within this XKTModel, in the order they were created.
*
* Each XKTTextureSet holds its position in this list in {@link XKTTextureSet#textureSetIndex}.
*
* Created by {@link XKTModel#finalize}.
*
* @type {XKTTextureSet[]}
*/
this.textureSetsList = [];
/**
* Map of {@link XKTMesh}s within this XKTModel, each mapped to {@link XKTMesh#meshId}.
*
* Created by {@link XKTModel#createMesh}.
*
* @type {{Number:XKTMesh}}
*/
this.meshes = {};
/**
* List of {@link XKTMesh}s within this XKTModel, in the order they were created.
*
* Each XKTMesh holds its position in this list in {@link XKTMesh#meshIndex}.
*
* Created by {@link XKTModel#finalize}.
*
* @type {XKTMesh[]}
*/
this.meshesList = [];
/**
* Map of {@link XKTEntity}s within this XKTModel, each mapped to {@link XKTEntity#entityId}.
*
* Created by {@link XKTModel#createEntity}.
*
* @type {{String:XKTEntity}}
*/
this.entities = {};
/**
* {@link XKTEntity}s within this XKTModel.
*
* Each XKTEntity holds its position in this list in {@link XKTEntity#entityIndex}.
*
* Created by {@link XKTModel#finalize}.
*
* @type {XKTEntity[]}
*/
this.entitiesList = [];
/**
* {@link XKTTile}s within this XKTModel.
*
* Created by {@link XKTModel#finalize}.
*
* @type {XKTTile[]}
*/
this.tilesList = [];
/**
* The axis-aligned 3D World-space boundary of this XKTModel.
*
* Created by {@link XKTModel#finalize}.
*
* @type {Float64Array}
*/
this.aabb = math.AABB3();
/**
* Indicates if this XKTModel has been finalized.
*
* Set ````true```` by {@link XKTModel#finalize}.
*
* @type {boolean}
*/
this.finalized = false;
}
/**
* Creates an {@link XKTPropertySet} within this XKTModel.
*
* Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
*
* @param {*} params Method parameters.
* @param {String} params.propertySetId Unique ID for the {@link XKTPropertySet}.
* @param {String} [params.propertySetType="default"] A meta type for the {@link XKTPropertySet}.
* @param {String} [params.propertySetName] Human-readable name for the {@link XKTPropertySet}. Defaults to the ````propertySetId```` parameter.
* @param {String[]} params.properties Properties for the {@link XKTPropertySet}.
* @returns {XKTPropertySet} The new {@link XKTPropertySet}.
*/
createPropertySet(params) {
if (!params) {
throw "[XKTModel.createPropertySet] Parameters expected: params";
}
if (params.propertySetId === null || params.propertySetId === undefined) {
throw "[XKTModel.createPropertySet] Parameter expected: params.propertySetId";
}
if (params.properties === null || params.properties === undefined) {
throw "[XKTModel.createPropertySet] Parameter expected: params.properties";
}
if (this.finalized) {
console.error("XKTModel has been finalized, can't add more property sets");
return;
}
if (this.propertySets[params.propertySetId]) {
// console.error("XKTPropertySet already exists with this ID: " + params.propertySetId);
return;
}
const propertySetId = params.propertySetId;
const propertySetType = params.propertySetType || "Default";
const propertySetName = params.propertySetName || params.propertySetId;
const properties = params.properties || [];
const propertySet = new XKTPropertySet(propertySetId, propertySetType, propertySetName, properties);
this.propertySets[propertySetId] = propertySet;
this.propertySetsList.push(propertySet);
return propertySet;
}
/**
* Creates an {@link XKTMetaObject} within this XKTModel.
*
* Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
*
* @param {*} params Method parameters.
* @param {String} params.metaObjectId Unique ID for the {@link XKTMetaObject}.
* @param {String} params.propertySetIds ID of one or more property sets that contains additional metadata about
* this {@link XKTMetaObject}. The property sets could be stored externally (ie not managed at all by the XKT file),
* or could be {@link XKTPropertySet}s within {@link XKTModel#propertySets}.
* @param {String} [params.metaObjectType="default"] A meta type for the {@link XKTMetaObject}. Can be anything,
* but is usually an IFC type, such as "IfcSite" or "IfcWall".
* @param {String} [params.metaObjectName] Human-readable name for the {@link XKTMetaObject}. Defaults to the ````metaObjectId```` parameter.
* @param {String} [params.parentMetaObjectId] ID of the parent {@link XKTMetaObject}, if any. Defaults to the ````metaObjectId```` parameter.
* @returns {XKTMetaObject} The new {@link XKTMetaObject}.
*/
createMetaObject(params) {
if (!params) {
throw "[XKTModel.createMetaObject] Parameters expected: params";
}
if (params.metaObjectId === null || params.metaObjectId === undefined) {
throw "[XKTModel.createMetaObject] Parameter expected: params.metaObjectId";
}
if (this.finalized) {
console.error("XKTModel has been finalized, can't add more meta objects");
return;
}
if (this.metaObjects[params.metaObjectId]) {
// console.error("XKTMetaObject already exists with this ID: " + params.metaObjectId);
return;
}
const metaObjectId = params.metaObjectId;
const propertySetIds = params.propertySetIds;
const metaObjectType = params.metaObjectType || "Default";
const metaObjectName = params.metaObjectName || params.metaObjectId;
const parentMetaObjectId = params.parentMetaObjectId;
const metaObject = new XKTMetaObject(metaObjectId, propertySetIds, metaObjectType, metaObjectName, parentMetaObjectId);
this.metaObjects[metaObjectId] = metaObject;
this.metaObjectsList.push(metaObject);
if (!parentMetaObjectId) {
if (!this._rootMetaObject) {
this._rootMetaObject = metaObject;
}
}
return metaObject;
}
/**
* Creates an {@link XKTTexture} within this XKTModel.
*
* Registers the new {@link XKTTexture} in {@link XKTModel#textures} and {@link XKTModel#texturesList}.
*
* Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
*
* @param {*} params Method parameters.
* @param {Number} params.textureId Unique ID for the {@link XKTTexture}.
* @param {String} [params.src] Source of an image file for the texture.
* @param {Buffer} [params.imageData] Image data for the texture.
* @param {Number} [params.mediaType] Media type (ie. MIME type) of ````imageData````. Supported values are {@link GIFMediaType}, {@link PNGMediaType} and {@link JPEGMediaType}.
* @param {Number} [params.width] Texture width, used with ````imageData````. Ignored for compressed textures.
* @param {Number} [params.height] Texture height, used with ````imageData````. Ignored for compressed textures.
* @param {Boolean} [params.compressed=true] Whether to compress the texture.
* @param {Number} [params.minFilter=LinearMipMapNearestFilter] How the texture is sampled when a texel covers less than one pixel. Supported
* values are {@link LinearMipmapLinearFilter}, {@link LinearMipMapNearestFilter}, {@link NearestMipMapNearestFilter},
* {@link NearestMipMapLinearFilter} and {@link LinearMipMapLinearFilter}. Ignored for compressed textures.
* @param {Number} [params.magFilter=LinearMipMapNearestFilter] How the texture is sampled when a texel covers more than one pixel. Supported values
* are {@link LinearFilter} and {@link NearestFilter}. Ignored for compressed textures.
* @param {Number} [params.wrapS=RepeatWrapping] Wrap parameter for texture coordinate *S*. Supported values are {@link ClampToEdgeWrapping},
* {@link MirroredRepeatWrapping} and {@link RepeatWrapping}. Ignored for compressed textures.
* @param {Number} [params.wrapT=RepeatWrapping] Wrap parameter for texture coordinate *T*. Supported values are {@link ClampToEdgeWrapping},
* {@link MirroredRepeatWrapping} and {@link RepeatWrapping}. Ignored for compressed textures.
* {@param {Number} [params.wrapR=RepeatWrapping] Wrap parameter for texture coordinate *R*. Supported values are {@link ClampToEdgeWrapping},
* {@link MirroredRepeatWrapping} and {@link RepeatWrapping}. Ignored for compressed textures.
* @returns {XKTTexture} The new {@link XKTTexture}.
*/
createTexture(params) {
if (!params) {
throw "[XKTModel.createTexture] Parameters expected: params";
}
if (params.textureId === null || params.textureId === undefined) {
throw "[XKTModel.createTexture] Parameter expected: params.textureId";
}
if (!params.imageData && !params.src) {
throw "[XKTModel.createTexture] Parameter expected: params.imageData or params.src";
}
if (this.finalized) {
console.error("XKTModel has been finalized, can't add more textures");
return;
}
if (this.textures[params.textureId]) {
console.error("XKTTexture already exists with this ID: " + params.textureId);
return;
}
if (params.src) {
const fileExt = params.src.split('.').pop();
if (fileExt !== "jpg" && fileExt !== "jpeg" && fileExt !== "png") {
console.error(`XKTModel does not support image files with extension '${fileExt}' - won't create texture '${params.textureId}`);
return;
}
}
const textureId = params.textureId;
const texture = new XKTTexture({
textureId,
imageData: params.imageData,
mediaType: params.mediaType,
minFilter: params.minFilter,
magFilter: params.magFilter,
wrapS: params.wrapS,
wrapT: params.wrapT,
wrapR: params.wrapR,
width: params.width,
height: params.height,
compressed: (params.compressed !== false),
src: params.src
});
this.textures[textureId] = texture;
this.texturesList.push(texture);
return texture;
}
/**
* Creates an {@link XKTTextureSet} within this XKTModel.
*
* Registers the new {@link XKTTextureSet} in {@link XKTModel#textureSets} and {@link XKTModel#.textureSetsList}.
*
* Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
*
* @param {*} params Method parameters.
* @param {Number} params.textureSetId Unique ID for the {@link XKTTextureSet}.
* @param {*} [params.colorTextureId] ID of *RGBA* base color {@link XKTTexture}, with color in *RGB* and alpha in *A*.
* @param {*} [params.metallicRoughnessTextureId] ID of *RGBA* metal-roughness {@link XKTTexture}, with the metallic factor in *R*, and roughness factor in *G*.
* @param {*} [params.normalsTextureId] ID of *RGBA* normal {@link XKTTexture}, with normal map vectors in *RGB*.
* @param {*} [params.emissiveTextureId] ID of *RGBA* emissive {@link XKTTexture}, with emissive color in *RGB*.
* @param {*} [params.occlusionTextureId] ID of *RGBA* occlusion {@link XKTTexture}, with occlusion factor in *R*.
* @returns {XKTTextureSet} The new {@link XKTTextureSet}.
*/
createTextureSet(params) {
if (!params) {
throw "[XKTModel.createTextureSet] Parameters expected: params";
}
if (params.textureSetId === null || params.textureSetId === undefined) {
throw "[XKTModel.createTextureSet] Parameter expected: params.textureSetId";
}
if (this.finalized) {
console.error("XKTModel has been finalized, can't add more textureSets");
return;
}
if (this.textureSets[params.textureSetId]) {
console.error("XKTTextureSet already exists with this ID: " + params.textureSetId);
return;
}
let colorTexture;
if (params.colorTextureId !== undefined && params.colorTextureId !== null) {
colorTexture = this.textures[params.colorTextureId];
if (!colorTexture) {
console.error(`Texture not found: ${params.colorTextureId} - ensure that you create it first with createTexture()`);
return;
}
colorTexture.channel = COLOR_TEXTURE;
}
let metallicRoughnessTexture;
if (params.metallicRoughnessTextureId !== undefined && params.metallicRoughnessTextureId !== null) {
metallicRoughnessTexture = this.textures[params.metallicRoughnessTextureId];
if (!metallicRoughnessTexture) {
console.error(`Texture not found: ${params.metallicRoughnessTextureId} - ensure that you create it first with createTexture()`);
return;
}
metallicRoughnessTexture.channel = METALLIC_ROUGHNESS_TEXTURE;
}
let normalsTexture;
if (params.normalsTextureId !== undefined && params.normalsTextureId !== null) {
normalsTexture = this.textures[params.normalsTextureId];
if (!normalsTexture) {
console.error(`Texture not found: ${params.normalsTextureId} - ensure that you create it first with createTexture()`);
return;
}
normalsTexture.channel = NORMALS_TEXTURE;
}
let emissiveTexture;
if (params.emissiveTextureId !== undefined && params.emissiveTextureId !== null) {
emissiveTexture = this.textures[params.emissiveTextureId];
if (!emissiveTexture) {
console.error(`Texture not found: ${params.emissiveTextureId} - ensure that you create it first with createTexture()`);
return;
}
emissiveTexture.channel = EMISSIVE_TEXTURE;
}
let occlusionTexture;
if (params.occlusionTextureId !== undefined && params.occlusionTextureId !== null) {
occlusionTexture = this.textures[params.occlusionTextureId];
if (!occlusionTexture) {
console.error(`Texture not found: ${params.occlusionTextureId} - ensure that you create it first with createTexture()`);
return;
}
occlusionTexture.channel = OCCLUSION_TEXTURE;
}
const textureSet = new XKTTextureSet({
textureSetId: params.textureSetId,
textureSetIndex: this.textureSetsList.length,
colorTexture,
metallicRoughnessTexture,
normalsTexture,
emissiveTexture,
occlusionTexture
});
this.textureSets[params.textureSetId] = textureSet;
this.textureSetsList.push(textureSet);
return textureSet;
}
/**
* Creates an {@link XKTGeometry} within this XKTModel.
*
* Registers the new {@link XKTGeometry} in {@link XKTModel#geometries} and {@link XKTModel#geometriesList}.
*
* Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
*
* @param {*} params Method parameters.
* @param {Number} params.geometryId Unique ID for the {@link XKTGeometry}.
* @param {String} params.primitiveType The type of {@link XKTGeometry}: "triangles", "lines" or "points".
* @param {Float64Array} params.positions Floating-point Local-space vertex positions for the {@link XKTGeometry}. Required for all primitive types.
* @param {Number[]} [params.normals] Floating-point vertex normals for the {@link XKTGeometry}. Only used with triangles primitives. Ignored for points and lines.
* @param {Number[]} [params.colors] Floating-point RGBA vertex colors for the {@link XKTGeometry}. Required for points primitives. Ignored for lines and triangles.
* @param {Number[]} [params.colorsCompressed] Integer RGBA vertex colors for the {@link XKTGeometry}. Required for points primitives. Ignored for lines and triangles.
* @param {Number[]} [params.uvs] Floating-point vertex UV coordinates for the {@link XKTGeometry}. Alias for ````uv````.
* @param {Number[]} [params.uv] Floating-point vertex UV coordinates for the {@link XKTGeometry}. Alias for ````uvs````.
* @param {Number[]} [params.colorsCompressed] Integer RGBA vertex colors for the {@link XKTGeometry}. Required for points primitives. Ignored for lines and triangles.
* @param {Uint32Array} [params.indices] Indices for the {@link XKTGeometry}. Required for triangles and lines primitives. Ignored for points.
* @param {Number} [params.edgeThreshold=10]
* @returns {XKTGeometry} The new {@link XKTGeometry}.
*/
createGeometry(params) {
if (!params) {
throw "[XKTModel.createGeometry] Parameters expected: params";
}
if (params.geometryId === null || params.geometryId === undefined) {
throw "[XKTModel.createGeometry] Parameter expected: params.geometryId";
}
if (!params.primitiveType) {
throw "[XKTModel.createGeometry] Parameter expected: params.primitiveType";
}
if (!params.positions) {
throw "[XKTModel.createGeometry] Parameter expected: params.positions";
}
const triangles = params.primitiveType === "triangles";
const points = params.primitiveType === "points";
const lines = params.primitiveType === "lines";
const line_strip = params.primitiveType === "line-strip";
const line_loop = params.primitiveType === "line-loop";
const triangle_strip = params.primitiveType === "triangle-strip";
const triangle_fan = params.primitiveType === "triangle-fan";
if (!triangles && !points && !lines && !line_strip && !line_loop) {
throw "[XKTModel.createGeometry] Unsupported value for params.primitiveType: "
+ params.primitiveType
+ "' - supported values are 'triangles', 'points', 'lines', 'line-strip', 'triangle-strip' and 'triangle-fan";
}
if (triangles) {
if (!params.indices) {
params.indices = this._createDefaultIndices()
throw "[XKTModel.createGeometry] Parameter expected for 'triangles' primitive: params.indices";
}
}
if (points) {
if (!params.colors && !params.colorsCompressed) {
console.error("[XKTModel.createGeometry] Parameter expected for 'points' primitive: params.colors or params.colorsCompressed");
return;
}
}
if (lines) {
if (!params.indices) {
throw "[XKTModel.createGeometry] Parameter expected for 'lines' primitive: params.indices";
}
}
if (this.finalized) {
console.error("XKTModel has been finalized, can't add more geometries");
return;
}
if (this.geometries[params.geometryId]) {
console.error("XKTGeometry already exists with this ID: " + params.geometryId);
return;
}
const geometryId = params.geometryId;
const primitiveType = params.primitiveType;
const positions = new Float64Array(params.positions); // May modify in #finalize
const xktGeometryCfg = {
geometryId: geometryId,
geometryIndex: this.geometriesList.length,
primitiveType: primitiveType,
positions: positions,
uvs: params.uvs || params.uv
}
if (triangles) {
if (params.normals) {
xktGeometryCfg.normals = new Float32Array(params.normals);
}
if (params.indices) {
xktGeometryCfg.indices = params.indices;
} else {
xktGeometryCfg.indices = this._createDefaultIndices(positions.length / 3);
}
}
if (points) {
if (params.colorsCompressed) {
xktGeometryCfg.colorsCompressed = new Uint8Array(params.colorsCompressed);
} else {
const colors = params.colors;
const colorsCompressed = new Uint8Array(colors.length);
for (let i = 0, len = colors.length; i < len; i++) {
colorsCompressed[i] = Math.floor(colors[i] * 255);
}
xktGeometryCfg.colorsCompressed = colorsCompressed;
}
}
if (lines) {
xktGeometryCfg.indices = params.indices;
}
if (triangles) {
if (!params.normals && !params.uv && !params.uvs) {
// Building models often duplicate positions to allow face-aligned vertex normals; when we're not
// providing normals for a geometry, it becomes possible to merge duplicate vertex positions within it.
// TODO: Make vertex merging also merge normals?
const mergedPositions = [];
const mergedIndices = [];
mergeVertices(xktGeometryCfg.positions, xktGeometryCfg.indices, mergedPositions, mergedIndices);
xktGeometryCfg.positions = new Float64Array(mergedPositions);
xktGeometryCfg.indices = mergedIndices;
}
xktGeometryCfg.edgeIndices = buildEdgeIndices(xktGeometryCfg.positions, xktGeometryCfg.indices, null, params.edgeThreshold || this.edgeThreshold || 10);
}
const geometry = new XKTGeometry(xktGeometryCfg);
this.geometries[geometryId] = geometry;
this.geometriesList.push(geometry);
return geometry;
}
_createDefaultIndices(numIndices) {
const indices = [];
for (let i = 0; i < numIndices; i++) {
indices.push(i);
}
return indices;
}
/**
* Creates an {@link XKTMesh} within this XKTModel.
*
* An {@link XKTMesh} can be owned by one {@link XKTEntity}, which can own multiple {@link XKTMesh}es.
*
* Registers the new {@link XKTMesh} in {@link XKTModel#meshes} and {@link XKTModel#meshesList}.
*
* @param {*} params Method parameters.
* @param {Number} params.meshId Unique ID for the {@link XKTMesh}.
* @param {Number} params.geometryId ID of an existing {@link XKTGeometry} in {@link XKTModel#geometries}.
* @param {Number} [params.textureSetId] Unique ID of an {@link XKTTextureSet} in {@link XKTModel#textureSets}.
* @param {Float32Array} params.color RGB color for the {@link XKTMesh}, with each color component in range [0..1].
* @param {Number} [params.metallic=0] How metallic the {@link XKTMesh} is, in range [0..1]. A value of ````0```` indicates fully dielectric material, while ````1```` indicates fully metallic.
* @param {Number} [params.roughness=1] How rough the {@link XKTMesh} is, in range [0..1]. A value of ````0```` indicates fully smooth, while ````1```` indicates fully rough.
* @param {Number} params.opacity Opacity factor for the {@link XKTMesh}, in range [0..1].
* @param {Float64Array} [params.matrix] Modeling matrix for the {@link XKTMesh}. Overrides ````position````, ````scale```` and ````rotation```` parameters.
* @param {Number[]} [params.position=[0,0,0]] Position of the {@link XKTMesh}. Overridden by the ````matrix```` parameter.
* @param {Number[]} [params.scale=[1,1,1]] Scale of the {@link XKTMesh}. Overridden by the ````matrix```` parameter.
* @param {Number[]} [params.rotation=[0,0,0]] Rotation of the {@link XKTMesh} as Euler angles given in degrees, for each of the X, Y and Z axis. Overridden by the ````matrix```` parameter.
* @returns {XKTMesh} The new {@link XKTMesh}.
*/
createMesh(params) {
if (params.meshId === null || params.meshId === undefined) {
throw "[XKTModel.createMesh] Parameter expected: params.meshId";
}
if (params.geometryId === null || params.geometryId === undefined) {
throw "[XKTModel.createMesh] Parameter expected: params.geometryId";
}
if (this.finalized) {
throw "[XKTModel.createMesh] XKTModel has been finalized, can't add more meshes";
}
if (this.meshes[params.meshId]) {
console.error("XKTMesh already exists with this ID: " + params.meshId);
return;
}
const geometry = this.geometries[params.geometryId];
if (!geometry) {
console.error("XKTGeometry not found: " + params.geometryId);
return;
}
geometry.numInstances++;
let textureSet = null;
if (params.textureSetId) {
textureSet = this.textureSets[params.textureSetId];
if (!textureSet) {
console.error("XKTTextureSet not found: " + params.textureSetId);
return;
}
textureSet.numInstances++;
}
let matrix = params.matrix;
if (!matrix) {
const position = params.position;
const scale = params.scale;
const rotation = params.rotation;
if (position || scale || rotation) {
matrix = math.identityMat4();
const quaternion = math.eulerToQuaternion(rotation || [0, 0, 0], "XYZ", math.identityQuaternion());
math.composeMat4(position || [0, 0, 0], quaternion, scale || [1, 1, 1], matrix)
} else {
matrix = math.identityMat4();
}
}
const meshIndex = this.meshesList.length;
const mesh = new XKTMesh({
meshId: params.meshId,
meshIndex,
matrix,
geometry,
color: params.color,
metallic: params.metallic,
roughness: params.roughness,
opacity: params.opacity,
textureSet
});
this.meshes[mesh.meshId] = mesh;
this.meshesList.push(mesh);
return mesh;
}
/**
* Creates an {@link XKTEntity} within this XKTModel.
*
* Registers the new {@link XKTEntity} in {@link XKTModel#entities} and {@link XKTModel#entitiesList}.
*
* Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
*
* @param {*} params Method parameters.
* @param {String} params.entityId Unique ID for the {@link XKTEntity}.
* @param {String[]} params.meshIds IDs of {@link XKTMesh}es used by the {@link XKTEntity}. Note that each {@link XKTMesh} can only be used by one {@link XKTEntity}.
* @returns {XKTEntity} The new {@link XKTEntity}.
*/
createEntity(params) {
if (!params) {
throw "[XKTModel.createEntity] Parameters expected: params";
}
if (params.entityId === null || params.entityId === undefined) {
throw "[XKTModel.createEntity] Parameter expected: params.entityId";
}
if (!params.meshIds) {
throw "[XKTModel.createEntity] Parameter expected: params.meshIds";
}
if (this.finalized) {
console.error("XKTModel has been finalized, can't add more entities");
return;
}
if (params.meshIds.length === 0) {
console.warn("XKTEntity has no meshes - won't create: " + params.entityId);
return;
}
let entityId = params.entityId;
if (this.entities[entityId]) {
while (this.entities[entityId]) {
entityId = math.createUUID();
}
console.error("XKTEntity already exists with this ID: " + params.entityId + " - substituting random ID instead: " + entityId);
}
const meshIds = params.meshIds;
const meshes = [];
for (let meshIdIdx = 0, meshIdLen = meshIds.length; meshIdIdx < meshIdLen; meshIdIdx++) {
const meshId = meshIds[meshIdIdx];
const mesh = this.meshes[meshId];
if (!mesh) {
console.error("XKTMesh found: " + meshId);
continue;
}
if (mesh.entity) {
console.error("XKTMesh " + meshId + " already used by XKTEntity " + mesh.entity.entityId);
continue;
}
meshes.push(mesh);
}
const entity = new XKTEntity(entityId, meshes);
for (let i = 0, len = meshes.length; i < len; i++) {
const mesh = meshes[i];
mesh.entity = entity;
}
this.entities[entityId] = entity;
this.entitiesList.push(entity);
return entity;
}
/**
* Creates a default {@link XKTMetaObject} for each {@link XKTEntity} that does not already have one.
*/
createDefaultMetaObjects() {
for (let i = 0, len = this.entitiesList.length; i < len; i++) {
const entity = this.entitiesList[i];
const metaObjectId = entity.entityId;
const metaObject = this.metaObjects[metaObjectId];
if (!metaObject) {
if (!this._rootMetaObject) {
this._rootMetaObject = this.createMetaObject({
metaObjectId: this.modelId,
metaObjectType: "Default",
metaObjectName: this.modelId
});
}
this.createMetaObject({
metaObjectId: metaObjectId,
metaObjectType: "Default",
metaObjectName: "" + metaObjectId,
parentMetaObjectId: this._rootMetaObject.metaObjectId
});
}
}
}
/**
* Finalizes this XKTModel.
*
* After finalizing, we may then serialize the model to an array buffer using {@link writeXKTModelToArrayBuffer}.
*
* Logs error and does nothing if this XKTModel has already been finalized.
*
* Internally, this method:
*
* * for each {@link XKTEntity} that doesn't already have a {@link XKTMetaObject}, creates one with {@link XKTMetaObject#metaObjectType} set to "default"
* * sets each {@link XKTEntity}'s {@link XKTEntity#hasReusedGeometries} true if it shares its {@link XKTGeometry}s with other {@link XKTEntity}s,
* * creates each {@link XKTEntity}'s {@link XKTEntity#aabb},
* * creates {@link XKTTile}s in {@link XKTModel#tilesList}, and
* * sets {@link XKTModel#finalized} ````true````.
*/
async finalize() {
if (this.finalized) {
console.log("XKTModel already finalized");
return;
}
this._removeUnusedTextures();
await this._compressTextures();
this._bakeSingleUseGeometryPositions();
this._bakeAndOctEncodeNormals();
this._createEntityAABBs();
const rootKDNode = this._createKDTree();
this.entitiesList = [];
this._createTilesFromKDTree(rootKDNode);
this._createReusedGeometriesDecodeMatrix();
this._flagSolidGeometries();
this.aabb.set(rootKDNode.aabb);
this.finalized = true;
}
_removeUnusedTextures() {
let texturesList = [];
const textures = {};
for (let i = 0, leni = this.texturesList.length; i < leni; i++) {
const texture = this.texturesList[i];
if (texture.channel !== null) {
texture.textureIndex = texturesList.length;
texturesList.push(texture);
textures[texture.textureId] = texture;
}
}
this.texturesList = texturesList;
this.textures = textures;
}
_compressTextures() {
let countTextures = this.texturesList.length;
return new Promise((resolve) => {
if (countTextures === 0) {
resolve();
return;
}
for (let i = 0, leni = this.texturesList.length; i < leni; i++) {
const texture = this.texturesList[i];
const encodingOptions = TEXTURE_ENCODING_OPTIONS[texture.channel] || {};
if (texture.src) {
// XKTTexture created with XKTModel#createTexture({ src: ... })
const src = texture.src;
const fileExt = src.split('.').pop();
switch (fileExt) {
case "jpeg":
case "jpg":
case "png":
load(src, ImageLoader, {
image: {
type: "data"
}
}).then((imageData) => {
if (texture.compressed) {
encode(imageData, KTX2BasisWriter, encodingOptions).then((encodedData) => {
const encodedImageData = new Uint8Array(encodedData);
texture.imageData = encodedImageData;
if (--countTextures <= 0) {
resolve();
}
}).catch((err) => {
console.error("[XKTModel.finalize] Failed to encode image: " + err);
if (--countTextures <= 0) {
resolve();
}
});
} else {
texture.imageData = new Uint8Array(1);
if (--countTextures <= 0) {
resolve();
}
}
}).catch((err) => {
console.error("[XKTModel.finalize] Failed to load image: " + err);
if (--countTextures <= 0) {
resolve();
}
});
break;
default:
if (--countTextures <= 0) {
resolve();
}
break;
}
}
if (texture.imageData) {
// XKTTexture created with XKTModel#createTexture({ imageData: ... })
if (texture.compressed) {
encode(texture.imageData, KTX2BasisWriter, encodingOptions)
.then((encodedImageData) => {
texture.imageData = new Uint8Array(encodedImageData);
if (--countTextures <= 0) {
resolve();
}
}).catch((err) => {
console.error("[XKTModel.finalize] Failed to encode image: " + err);
if (--countTextures <= 0) {
resolve();
}
});
} else {
texture.imageData = new Uint8Array(1);
if (--countTextures <= 0) {
resolve();
}
}
}
}
});
}
_bakeSingleUseGeometryPositions() {
for (let j = 0, lenj = this.meshesList.length; j < lenj; j++) {
const mesh = this.meshesList[j];
const geometry = mesh.geometry;
if (geometry.numInstances === 1) {
const matrix = mesh.matrix;
if (matrix && (!math.isIdentityMat4(matrix))) {
const positions = geometry.positions;
for (let i = 0, len = positions.length; i < len; i += 3) {
tempVec4a[0] = positions[i + 0];
tempVec4a[1] = positions[i + 1];
tempVec4a[2] = positions[i + 2];
tempVec4a[3] = 1;
math.transformPoint4(matrix, tempVec4a, tempVec4b);
positions[i + 0] = tempVec4b[0];
positions[i + 1] = tempVec4b[1];
positions[i + 2] = tempVec4b[2];
}
}
}
}
}
_bakeAndOctEncodeNormals() {
for (let i = 0, len = this.meshesList.length; i < len; i++) {
const mesh = this.meshesList[i];
const geometry = mesh.geometry;
if (geometry.normals && !geometry.normalsOctEncoded) {
geometry.normalsOctEncoded = new Int8Array(geometry.normals.length);
if (geometry.numInstances > 1) {
geometryCompression.octEncodeNormals(geometry.normals, geometry.normals.length, geometry.normalsOctEncoded, 0);
} else {
const modelNormalMatrix = math.inverseMat4(math.transposeMat4(mesh.matrix, tempMat4), tempMat4b);
geometryCompression.transformAndOctEncodeNormals(modelNormalMatrix, geometry.normals, geometry.normals.length, geometry.normalsOctEncoded, 0);
}
}
}
}
_createEntityAABBs() {
for (let i = 0, len = this.entitiesList.length; i < len; i++) {
const entity = this.entitiesList[i];
const entityAABB = entity.aabb;
const meshes = entity.meshes;
math.collapseAABB3(entityAABB);
for (let j = 0, lenj = meshes.length; j < lenj; j++) {
const mesh = meshes[j];
const geometry = mesh.geometry;
const matrix = mesh.matrix;
if (geometry.numInstances > 1) {
const positions = geometry.positions;
for (let i = 0, len = positions.length; i < len; i += 3) {
tempVec4a[0] = positions[i + 0];
tempVec4a[1] = positions[i + 1];
tempVec4a[2] = positions[i + 2];
tempVec4a[3] = 1;
math.transformPoint4(matrix, tempVec4a, tempVec4b);
math.expandAABB3Point3(entityAABB, tempVec4b);
}
} else {
const positions = geometry.positions;
for (let i = 0, len = positions.length; i < len; i += 3) {
tempVec4a[0] = positions[i + 0];
tempVec4a[1] = positions[i + 1];
tempVec4a[2] = positions[i + 2];
math.expandAABB3Point3(entityAABB, tempVec4a);
}
}
}
}
}
_createKDTree() {
let aabb;
if (this.modelAABB) {
aabb = this.modelAABB; // Pre-known uber AABB
} else {
aabb = math.collapseAABB3();
for (let i = 0, len = this.entitiesList.length; i < len; i++) {
const entity = this.entitiesList[i];
math.expandAABB3(aabb, entity.aabb);
}
}
const rootKDNode = new KDNode(aabb);
for (let i = 0, len = this.entitiesList.length; i < len; i++) {
const entity = this.entitiesList[i];
this._insertEntityIntoKDTree(rootKDNode, entity);
}
return rootKDNode;
}
_insertEntityIntoKDTree(kdNode, entity) {
const nodeAABB = kdNode.aabb;
const entityAABB = entity.aabb;
const nodeAABBDiag = math.getAABB3Diag(nodeAABB);
if (nodeAABBDiag < this.minTileSize) {
kdNode.entities = kdNode.entities || [];
kdNode.entities.push(entity);
math.expandAABB3(nodeAABB, entityAABB);
return;
}
if (kdNode.left) {
if (math.containsAABB3(kdNode.left.aabb, entityAABB)) {
this._insertEntityIntoKDTree(kdNode.left, entity);
return;
}
}
if (kdNode.right) {
if (math.containsAABB3(kdNode.right.aabb, entityAABB)) {
this._insertEntityIntoKDTree(kdNode.right, entity);
return;
}
}
kdTreeDimLength[0] = nodeAABB[3] - nodeAABB[0];
kdTreeDimLength[1] = nodeAABB[4] - nodeAABB[1];
kdTreeDimLength[2] = nodeAABB[5] - nodeAABB[2];
let dim = 0;
if (kdTreeDimLength[1] > kdTreeDimLength[dim]) {
dim = 1;
}
if (kdTreeDimLength[2] > kdTreeDimLength[dim]) {
dim = 2;
}
if (!kdNode.left) {
const aabbLeft = nodeAABB.slice();
aabbLeft[dim + 3] = ((nodeAABB[dim] + nodeAABB[dim + 3]) / 2.0);
kdNode.left = new KDNode(aabbLeft);
if (math.containsAABB3(aabbLeft, entityAABB)) {
this._insertEntityIntoKDTree(kdNode.left, entity);
return;
}
}
if (!kdNode.right) {
const aabbRight = nodeAABB.slice();
aabbRight[dim] = ((nodeAABB[dim] + nodeAABB[dim + 3]) / 2.0);
kdNode.right = new KDNode(aabbRight);
if (math.containsAABB3(aabbRight, entityAABB)) {
this._insertEntityIntoKDTree(kdNode.right, entity);
return;
}
}
kdNode.entities = kdNode.entities || [];
kdNode.entities.push(entity);
math.expandAABB3(nodeAABB, entityAABB);
}
_createTilesFromKDTree(rootKDNode) {
this._createTilesFromKDNode(rootKDNode);
}
_createTilesFromKDNode(kdNode) {
if (kdNode.entities && kdNode.entities.length > 0) {
this._createTileFromEntities(kdNode);
}
if (kdNode.left) {
this._createTilesFromKDNode(kdNode.left);
}
if (kdNode.right) {
this._createTilesFromKDNode(kdNode.right);
}
}
/**
* Creates a tile from the given entities.
*
* For each single-use {@link XKTGeometry}, this method centers {@link XKTGeometry#positions} to make them relative to the
* tile's center, then quantizes the positions to unsigned 16-bit integers, relative to the tile's boundary.
*
* @param kdNode
*/
_createTileFromEntities(kdNode) {
const tileAABB = kdNode.aabb;
const entities = kdNode.entities;
const tileCenter = math.getAABB3Center(tileAABB);
const tileCenterNeg = math.mulVec3Scalar(tileCenter, -1, math.vec3());
const rtcAABB = math.AABB3(); // AABB centered at the RTC origin
rtcAABB[0] = tileAABB[0] - tileCenter[0];
rtcAABB[1] = tileAABB[1] - tileCenter[1];
rtcAABB[2] = tileAABB[2] - tileCenter[2];
rtcAABB[3] = tileAABB[3] - tileCenter[0];
rtcAABB[4] = tileAABB[4] - tileCenter[1];
rtcAABB[5] = tileAABB[5] - tileCenter[2];
for (let i = 0; i < entities.length; i++) {
const entity = entities [i];
const meshes = entity.meshes;
for (let j = 0, lenj = meshes.length; j < lenj; j++) {
const mesh = meshes[j];
const geometry = mesh.geometry;
if (!geometry.reused) { // Batched geometry
const positions = geometry.positions;
// Center positions relative to their tile's World-space center
for (let k = 0, lenk = positions.length; k < lenk; k += 3) {
positions[k + 0] -= tileCenter[0];
positions[k + 1] -= tileCenter[1];
positions[k + 2] -= tileCenter[2];
}
// Quantize positions relative to tile's RTC-space boundary
geometryCompression.quantizePositions(positions, positions.length, rtcAABB, geometry.positionsQuantized);
} else { // Instanced geometry
// Post-multiply a translation to the mesh's modeling matrix
// to center the entity's geometry instances to the tile RTC center
//////////////////////////////
// Why do we do this?
// Seems to break various models
/////////////////////////////////
math.translateMat4v(tileCenterNeg, mesh.matrix);
}
}
entity.entityIndex = this.entitiesList.length;
this.entitiesList.push(entity);
}
const tile = new XKTTile(tileAABB, entities);
this.tilesList.push(tile);
}
_createReusedGeometriesDecodeMatrix() {
const tempVec3a = math.vec3();
const reusedGeometriesAABB = math.collapseAABB3(math.AABB3());
let countReusedGeometries = 0;
for (let geometryIndex = 0, numGeometries = this.geometriesList.length; geometryIndex < numGeometries; geometryIndex++) {
const geometry = this.geometriesList [geometryIndex];
if (geometry.reused) { // Instanced geometry
const positions = geometry.positions;
for (let i = 0, len = positions.length; i < len; i += 3) {
tempVec3a[0] = positions[i];
tempVec3a[1] = positions[i + 1];
tempVec3a[2] = positions[i + 2];
math.expandAABB3Point3(reusedGeometriesAABB, tempVec3a);
}
countReusedGeometries++;
}
}
if (countReusedGeometries > 0) {
geometryCompression.createPositionsDecodeMatrix(reusedGeometriesAABB, this.reusedGeometriesDecodeMatrix);
for (let geometryIndex = 0, numGeometries = this.geometriesList.length; geometryIndex < numGeometries; geometryIndex++) {
const geometry = this.geometriesList [geometryIndex];
if (geometry.reused) {
geometryCompression.quantizePositions(geometry.positions, geometry.positions.length, reusedGeometriesAABB, geometry.positionsQuantized);
}
}
} else {
math.identityMat4(this.reusedGeometriesDecodeMatrix); // No need for this matrix, but we'll be tidy and set it to identity
}
}
_flagSolidGeometries() {
let maxNumPositions = 0;
let maxNumIndices = 0;
for (let i = 0, len = this.geometriesList.length; i < len; i++) {
const geometry = this.geometriesList[i];
if (geometry.primitiveType === "triangles") {
if (geometry.positionsQuantized.length > maxNumPositions) {
maxNumPositions = geometry.positionsQuantized.length;
}
if (geometry.indices.length > maxNumIndices) {
maxNumIndices = geometry.indices.length;
}
}
}
let vertexIndexMapping = new Array(maxNumPositions / 3);
let edges = new Array(maxNumIndices);
for (let i = 0, len = this.geometriesList.length; i < len; i++) {
const geometry = this.geometriesList[i];
if (geometry.primitiveType === "triangles") {
geometry.solid = isTriangleMeshSolid(geometry.indices, geometry.positionsQuantized, vertexIndexMapping, edges);
}
}
}
}
export {
XKTModel
}