@babylonjs/viewer
Version:
The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.
626 lines (620 loc) • 29.3 kB
JavaScript
import { b as Tools, aI as Geometry, m as VertexBuffer, L as Logger, bJ as unregisterGLTFExtension, bI as registerGLTFExtension } from './index-FzOfPXLV.esm.js';
import { A as AutoReleaseWorkerPool } from './workerPool-DP4ZCjkC.esm.js';
import { GLTFLoader, ArrayItem, LoadBoundingInfoFromPositionAccessor } from './glTFLoader-VVyk3AwX.esm.js';
import './bone--mL3H5cQ.esm.js';
import './skeleton-B7-ffRm_.esm.js';
import './rawTexture-B2DimmQ5.esm.js';
import './assetContainer-8JZnoDWQ.esm.js';
import './objectModelMapping-CJnQ6at_.esm.js';
/**
* @internal
*/
/**
* @internal
*/
function DecodeMesh(module /** DecoderModule */, data, attributeIDs, onIndicesData, onAttributeData) {
const decoderModule = module;
let decoder = null;
let buffer = null;
let geometry = null;
try {
decoder = new decoderModule.Decoder();
buffer = new decoderModule.DecoderBuffer();
buffer.Init(data, data.byteLength);
let status;
const type = decoder.GetEncodedGeometryType(buffer);
switch (type) {
case decoderModule.TRIANGULAR_MESH: {
const mesh = new decoderModule.Mesh();
status = decoder.DecodeBufferToMesh(buffer, mesh);
if (!status.ok() || mesh.ptr === 0) {
throw new Error(status.error_msg());
}
const numFaces = mesh.num_faces();
const numIndices = numFaces * 3;
const byteLength = numIndices * 4;
const ptr = decoderModule._malloc(byteLength);
try {
decoder.GetTrianglesUInt32Array(mesh, byteLength, ptr);
const indices = new Uint32Array(numIndices);
indices.set(new Uint32Array(decoderModule.HEAPF32.buffer, ptr, numIndices));
onIndicesData(indices);
}
finally {
decoderModule._free(ptr);
}
geometry = mesh;
break;
}
case decoderModule.POINT_CLOUD: {
const pointCloud = new decoderModule.PointCloud();
status = decoder.DecodeBufferToPointCloud(buffer, pointCloud);
if (!status.ok() || !pointCloud.ptr) {
throw new Error(status.error_msg());
}
geometry = pointCloud;
break;
}
default: {
throw new Error(`Invalid geometry type ${type}`);
}
}
const numPoints = geometry.num_points();
const processAttribute = (decoder, geometry, kind, attribute /** Attribute */) => {
const dataType = attribute.data_type();
const numComponents = attribute.num_components();
const normalized = attribute.normalized();
const byteStride = attribute.byte_stride();
const byteOffset = attribute.byte_offset();
const dataTypeInfo = {
[decoderModule.DT_FLOAT32]: { typedArrayConstructor: Float32Array, heap: decoderModule.HEAPF32 },
[decoderModule.DT_INT8]: { typedArrayConstructor: Int8Array, heap: decoderModule.HEAP8 },
[decoderModule.DT_INT16]: { typedArrayConstructor: Int16Array, heap: decoderModule.HEAP16 },
[decoderModule.DT_INT32]: { typedArrayConstructor: Int32Array, heap: decoderModule.HEAP32 },
[decoderModule.DT_UINT8]: { typedArrayConstructor: Uint8Array, heap: decoderModule.HEAPU8 },
[decoderModule.DT_UINT16]: { typedArrayConstructor: Uint16Array, heap: decoderModule.HEAPU16 },
[decoderModule.DT_UINT32]: { typedArrayConstructor: Uint32Array, heap: decoderModule.HEAPU32 },
};
const info = dataTypeInfo[dataType];
if (!info) {
throw new Error(`Invalid data type ${dataType}`);
}
const numValues = numPoints * numComponents;
const byteLength = numValues * info.typedArrayConstructor.BYTES_PER_ELEMENT;
const ptr = decoderModule._malloc(byteLength);
try {
decoder.GetAttributeDataArrayForAllPoints(geometry, attribute, dataType, byteLength, ptr);
const data = new info.typedArrayConstructor(info.heap.buffer, ptr, numValues);
onAttributeData(kind, data.slice(), numComponents, byteOffset, byteStride, normalized);
}
finally {
decoderModule._free(ptr);
}
};
if (attributeIDs) {
for (const kind in attributeIDs) {
const id = attributeIDs[kind];
const attribute = decoder.GetAttributeByUniqueId(geometry, id);
processAttribute(decoder, geometry, kind, attribute);
}
}
else {
const dracoAttributeTypes = {
position: decoderModule.POSITION,
normal: decoderModule.NORMAL,
color: decoderModule.COLOR,
uv: decoderModule.TEX_COORD,
};
for (const kind in dracoAttributeTypes) {
const id = decoder.GetAttributeId(geometry, dracoAttributeTypes[kind]);
if (id !== -1) {
const attribute = decoder.GetAttribute(geometry, id);
processAttribute(decoder, geometry, kind, attribute);
}
}
}
return numPoints;
}
finally {
if (geometry) {
decoderModule.destroy(geometry);
}
if (buffer) {
decoderModule.destroy(buffer);
}
if (decoder) {
decoderModule.destroy(decoder);
}
}
}
/**
* The worker function that gets converted to a blob url to pass into a worker.
* To be used if a developer wants to create their own worker instance and inject it instead of using the default worker.
*/
function DecoderWorkerFunction() {
let decoderPromise;
onmessage = (event) => {
const message = event.data;
switch (message.id) {
case "init": {
// if URL is provided then load the script. Otherwise expect the script to be loaded already
if (message.url) {
importScripts(message.url);
}
const initDecoderObject = message.wasmBinary ? { wasmBinary: message.wasmBinary } : {};
decoderPromise = DracoDecoderModule(initDecoderObject);
postMessage({ id: "initDone" });
break;
}
case "decodeMesh": {
if (!decoderPromise) {
throw new Error("Draco decoder module is not available");
}
// eslint-disable-next-line github/no-then
decoderPromise.then((decoder) => {
const numPoints = DecodeMesh(decoder, message.dataView, message.attributes, (indices) => {
postMessage({ id: "indices", data: indices }, [indices.buffer]);
}, (kind, data, size, offset, stride, normalized) => {
postMessage({ id: "attribute", kind, data, size, byteOffset: offset, byteStride: stride, normalized }, [data.buffer]);
});
postMessage({ id: "decodeMeshDone", totalVertices: numPoints });
});
break;
}
}
};
}
/**
* Initializes a worker that was created for the draco agent pool
* @param worker The worker to initialize
* @param wasmBinary The wasm binary to load into the worker
* @param moduleUrl The url to the draco decoder module (optional)
* @returns A promise that resolves when the worker is initialized
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
async function initializeWebWorker(worker, wasmBinary, moduleUrl) {
return await new Promise((resolve, reject) => {
const onError = (error) => {
worker.removeEventListener("error", onError);
worker.removeEventListener("message", onMessage);
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(error);
};
const onMessage = (event) => {
if (event.data.id === "initDone") {
worker.removeEventListener("error", onError);
worker.removeEventListener("message", onMessage);
resolve(worker);
}
};
worker.addEventListener("error", onError);
worker.addEventListener("message", onMessage);
// Load with either JS-only or WASM version
if (!wasmBinary) {
worker.postMessage({
id: "init",
url: moduleUrl,
});
}
else {
// clone the array buffer to make it transferable
const clone = wasmBinary.slice(0);
worker.postMessage({
id: "init",
url: moduleUrl,
wasmBinary: clone,
}, [clone]);
}
// note: no transfer list as the ArrayBuffer is shared across main thread and pool workers
});
}
/**
* @internal
*/
function _GetDefaultNumWorkers() {
if (typeof navigator !== "object" || !navigator.hardwareConcurrency) {
return 1;
}
// Use 50% of the available logical processors but capped at 4.
return Math.min(Math.floor(navigator.hardwareConcurrency * 0.5), 4);
}
/**
* @internal
*/
function _IsConfigurationAvailable(config) {
return !!((config.wasmUrl && (config.wasmBinary || config.wasmBinaryUrl) && typeof WebAssembly === "object") || config.fallbackUrl);
// TODO: Account for jsModule
}
/**
* Base class for a Draco codec.
* @internal
*/
class DracoCodec {
/**
* Constructor
* @param configuration The configuration for the DracoCodec instance.
*/
constructor(configuration) {
// check if the codec binary and worker pool was injected
// Note - it is expected that the developer checked if WebWorker, WebAssembly and the URL object are available
if (configuration.workerPool) {
// Set the promise accordingly
this._workerPoolPromise = Promise.resolve(configuration.workerPool);
return;
}
// to avoid making big changes to the code here, if wasmBinary is provided use it in the wasmBinaryPromise
const wasmBinaryProvided = configuration.wasmBinary;
const numberOfWorkers = configuration.numWorkers ?? _GetDefaultNumWorkers();
const useWorkers = numberOfWorkers && typeof Worker === "function" && typeof URL === "function";
const urlNeeded = useWorkers || !configuration.jsModule;
// code maintained here for back-compat with no changes
const codecInfo = configuration.wasmUrl && configuration.wasmBinaryUrl && typeof WebAssembly === "object"
? {
url: urlNeeded ? Tools.GetBabylonScriptURL(configuration.wasmUrl, true) : "",
wasmBinaryPromise: wasmBinaryProvided
? Promise.resolve(wasmBinaryProvided)
: Tools.LoadFileAsync(Tools.GetBabylonScriptURL(configuration.wasmBinaryUrl, true)),
}
: {
url: urlNeeded ? Tools.GetBabylonScriptURL(configuration.fallbackUrl) : "",
wasmBinaryPromise: Promise.resolve(undefined),
};
// If using workers, initialize a worker pool with either the wasm or url?
if (useWorkers) {
// eslint-disable-next-line github/no-then
this._workerPoolPromise = codecInfo.wasmBinaryPromise.then((wasmBinary) => {
const workerContent = this._getWorkerContent();
const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" }));
// eslint-disable-next-line @typescript-eslint/promise-function-async
return new AutoReleaseWorkerPool(numberOfWorkers, () => {
const worker = new Worker(workerBlobUrl);
return initializeWebWorker(worker, wasmBinary, codecInfo.url);
});
});
}
else {
// eslint-disable-next-line github/no-then
this._modulePromise = codecInfo.wasmBinaryPromise.then(async (wasmBinary) => {
if (!this._isModuleAvailable()) {
if (!configuration.jsModule) {
if (!codecInfo.url) {
throw new Error("Draco codec module is not available");
}
await Tools.LoadBabylonScriptAsync(codecInfo.url);
}
}
return await this._createModuleAsync(wasmBinary, configuration.jsModule);
});
}
}
/**
* Returns a promise that resolves when ready. Call this manually to ensure the draco codec is ready before use.
* @returns a promise that resolves when ready
*/
async whenReadyAsync() {
if (this._workerPoolPromise) {
await this._workerPoolPromise;
return;
}
if (this._modulePromise) {
await this._modulePromise;
return;
}
}
/**
* Stop all async operations and release resources.
*/
dispose() {
if (this._workerPoolPromise) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then
this._workerPoolPromise.then((workerPool) => {
workerPool.dispose();
});
}
delete this._workerPoolPromise;
delete this._modulePromise;
}
}
/**
* @experimental This class is an experimental version of `DracoCompression` and is subject to change.
*
* Draco Decoder (https://google.github.io/draco/)
*
* This class wraps the Draco decoder module.
*
* By default, the configuration points to a copy of the Draco decoder files for glTF from the Babylon.js cdn https://cdn.babylonjs.com/draco_wasm_wrapper_gltf.js.
*
* To update the configuration, use the following code:
* ```javascript
* DracoDecoder.DefaultConfiguration = {
* wasmUrl: "<url to the WebAssembly library>",
* wasmBinaryUrl: "<url to the WebAssembly binary>",
* fallbackUrl: "<url to the fallback JavaScript library>",
* };
* ```
*
* Draco has two versions, one for WebAssembly and one for JavaScript. The decoder configuration can be set to only support WebAssembly or only support the JavaScript version.
* Decoding will automatically fallback to the JavaScript version if WebAssembly version is not configured or if WebAssembly is not supported by the browser.
* Use `DracoDecoder.DefaultAvailable` to determine if the decoder configuration is available for the current context.
*
* To decode Draco compressed data, get the default DracoDecoder object and call decodeMeshToGeometryAsync:
* ```javascript
* var geometry = await DracoDecoder.Default.decodeMeshToGeometryAsync(data);
* ```
*/
class DracoDecoder extends DracoCodec {
/**
* Returns true if the decoder's `DefaultConfiguration` is available.
*/
static get DefaultAvailable() {
return _IsConfigurationAvailable(DracoDecoder.DefaultConfiguration);
}
/**
* Default instance for the DracoDecoder.
*/
static get Default() {
DracoDecoder._Default ??= new DracoDecoder();
return DracoDecoder._Default;
}
/**
* Reset the default DracoDecoder object to null and disposing the removed default instance.
* Note that if the workerPool is a member of the static DefaultConfiguration object it is recommended not to run dispose,
* unless the static worker pool is no longer needed.
* @param skipDispose set to true to not dispose the removed default instance
*/
static ResetDefault(skipDispose) {
if (DracoDecoder._Default) {
if (!skipDispose) {
DracoDecoder._Default.dispose();
}
DracoDecoder._Default = null;
}
}
_isModuleAvailable() {
return typeof DracoDecoderModule !== "undefined";
}
async _createModuleAsync(wasmBinary, jsModule /** DracoDecoderModule */) {
const module = await (jsModule || DracoDecoderModule)({ wasmBinary });
return { module };
}
_getWorkerContent() {
return `${DecodeMesh}(${DecoderWorkerFunction})()`;
}
/**
* Creates a new Draco decoder.
* @param configuration Optional override of the configuration for the DracoDecoder. If not provided, defaults to {@link DracoDecoder.DefaultConfiguration}.
*/
constructor(configuration = DracoDecoder.DefaultConfiguration) {
super(configuration);
}
/**
* Decode Draco compressed mesh data to mesh data.
* @param data The ArrayBuffer or ArrayBufferView of the compressed Draco data
* @param attributes A map of attributes from vertex buffer kinds to Draco unique ids
* @param gltfNormalizedOverride A map of attributes from vertex buffer kinds to normalized flags to override the Draco normalization
* @returns A promise that resolves with the decoded mesh data
*/
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
decodeMeshToMeshDataAsync(data, attributes, gltfNormalizedOverride) {
const dataView = data instanceof ArrayBuffer ? new Int8Array(data) : new Int8Array(data.buffer, data.byteOffset, data.byteLength);
const applyGltfNormalizedOverride = (kind, normalized) => {
if (gltfNormalizedOverride && gltfNormalizedOverride[kind] !== undefined) {
if (normalized !== gltfNormalizedOverride[kind]) {
Logger.Warn(`Normalized flag from Draco data (${normalized}) does not match normalized flag from glTF accessor (${gltfNormalizedOverride[kind]}). Using flag from glTF accessor.`);
}
return gltfNormalizedOverride[kind];
}
else {
return normalized;
}
};
if (this._workerPoolPromise) {
// eslint-disable-next-line github/no-then
return this._workerPoolPromise.then(async (workerPool) => {
return await new Promise((resolve, reject) => {
workerPool.push((worker, onComplete) => {
let resultIndices = null;
const resultAttributes = [];
const onError = (error) => {
worker.removeEventListener("error", onError);
worker.removeEventListener("message", onMessage);
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(error);
onComplete();
};
const onMessage = (event) => {
const message = event.data;
switch (message.id) {
case "indices": {
resultIndices = message.data;
break;
}
case "attribute": {
resultAttributes.push({
kind: message.kind,
data: message.data,
size: message.size,
byteOffset: message.byteOffset,
byteStride: message.byteStride,
normalized: applyGltfNormalizedOverride(message.kind, message.normalized),
});
break;
}
case "decodeMeshDone": {
worker.removeEventListener("error", onError);
worker.removeEventListener("message", onMessage);
resolve({ indices: resultIndices, attributes: resultAttributes, totalVertices: message.totalVertices });
onComplete();
break;
}
}
};
worker.addEventListener("error", onError);
worker.addEventListener("message", onMessage);
const dataViewCopy = dataView.slice();
worker.postMessage({ id: "decodeMesh", dataView: dataViewCopy, attributes: attributes }, [dataViewCopy.buffer]);
});
});
});
}
if (this._modulePromise) {
// eslint-disable-next-line github/no-then
return this._modulePromise.then((decoder) => {
let resultIndices = null;
const resultAttributes = [];
const numPoints = DecodeMesh(decoder.module, dataView, attributes, (indices) => {
resultIndices = indices;
}, (kind, data, size, byteOffset, byteStride, normalized) => {
resultAttributes.push({
kind,
data,
size,
byteOffset,
byteStride,
normalized,
});
});
return { indices: resultIndices, attributes: resultAttributes, totalVertices: numPoints };
});
}
throw new Error("Draco decoder module is not available");
}
/**
* Decode Draco compressed mesh data to Babylon geometry.
* @param name The name to use when creating the geometry
* @param scene The scene to use when creating the geometry
* @param data The ArrayBuffer or ArrayBufferView of the Draco compressed data
* @param attributes A map of attributes from vertex buffer kinds to Draco unique ids
* @returns A promise that resolves with the decoded geometry
*/
async decodeMeshToGeometryAsync(name, scene, data, attributes) {
const meshData = await this.decodeMeshToMeshDataAsync(data, attributes);
const geometry = new Geometry(name, scene);
if (meshData.indices) {
geometry.setIndices(meshData.indices);
}
for (const attribute of meshData.attributes) {
geometry.setVerticesBuffer(new VertexBuffer(scene.getEngine(), attribute.data, attribute.kind, false, undefined, attribute.byteStride, undefined, attribute.byteOffset, attribute.size, undefined, attribute.normalized, true), meshData.totalVertices);
}
return geometry;
}
/** @internal */
async _decodeMeshToGeometryForGltfAsync(name, scene, data, attributes, gltfNormalizedOverride, boundingInfo) {
const meshData = await this.decodeMeshToMeshDataAsync(data, attributes, gltfNormalizedOverride);
const geometry = new Geometry(name, scene);
if (boundingInfo) {
geometry._boundingInfo = boundingInfo;
geometry.useBoundingInfoFromGeometry = true;
}
if (meshData.indices) {
geometry.setIndices(meshData.indices);
}
for (const attribute of meshData.attributes) {
geometry.setVerticesBuffer(new VertexBuffer(scene.getEngine(), attribute.data, attribute.kind, false, undefined, attribute.byteStride, undefined, attribute.byteOffset, attribute.size, undefined, attribute.normalized, true), meshData.totalVertices);
}
return geometry;
}
}
/**
* Default configuration for the DracoDecoder. Defaults to the following:
* - numWorkers: 50% of the available logical processors, capped to 4. If no logical processors are available, defaults to 1.
* - wasmUrl: `"https://cdn.babylonjs.com/draco_wasm_wrapper_gltf.js"`
* - wasmBinaryUrl: `"https://cdn.babylonjs.com/draco_decoder_gltf.wasm"`
* - fallbackUrl: `"https://cdn.babylonjs.com/draco_decoder_gltf.js"`
*/
DracoDecoder.DefaultConfiguration = {
wasmUrl: `${Tools._DefaultCdnUrl}/draco_wasm_wrapper_gltf.js`,
wasmBinaryUrl: `${Tools._DefaultCdnUrl}/draco_decoder_gltf.wasm`,
fallbackUrl: `${Tools._DefaultCdnUrl}/draco_decoder_gltf.js`,
};
DracoDecoder._Default = null;
/* eslint-disable github/no-then */
const NAME = "KHR_draco_mesh_compression";
/**
* [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_draco_mesh_compression/README.md)
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
class KHR_draco_mesh_compression {
/**
* @internal
*/
constructor(loader) {
/**
* The name of this extension.
*/
this.name = NAME;
/**
* Defines whether to use the normalized flag from the glTF accessor instead of the Draco data. Defaults to true.
*/
this.useNormalizedFlagFromAccessor = true;
this._loader = loader;
this.enabled = DracoDecoder.DefaultAvailable && this._loader.isExtensionUsed(NAME);
}
/** @internal */
dispose() {
delete this.dracoDecoder;
this._loader = null;
}
/**
* @internal
*/
// eslint-disable-next-line no-restricted-syntax
_loadVertexDataAsync(context, primitive, babylonMesh) {
return GLTFLoader.LoadExtensionAsync(context, primitive, this.name, async (extensionContext, extension) => {
if (primitive.mode != undefined) {
if (primitive.mode !== 4 /* MeshPrimitiveMode.TRIANGLES */ && primitive.mode !== 5 /* MeshPrimitiveMode.TRIANGLE_STRIP */) {
throw new Error(`${context}: Unsupported mode ${primitive.mode}`);
}
}
const attributes = {};
const normalized = {};
const loadAttribute = (name, kind) => {
const uniqueId = extension.attributes[name];
if (uniqueId == undefined) {
return;
}
babylonMesh._delayInfo = babylonMesh._delayInfo || [];
if (babylonMesh._delayInfo.indexOf(kind) === -1) {
babylonMesh._delayInfo.push(kind);
}
attributes[kind] = uniqueId;
if (this.useNormalizedFlagFromAccessor) {
const accessor = ArrayItem.TryGet(this._loader.gltf.accessors, primitive.attributes[name]);
if (accessor) {
normalized[kind] = accessor.normalized || false;
}
}
};
loadAttribute("POSITION", VertexBuffer.PositionKind);
loadAttribute("NORMAL", VertexBuffer.NormalKind);
loadAttribute("TANGENT", VertexBuffer.TangentKind);
loadAttribute("TEXCOORD_0", VertexBuffer.UVKind);
loadAttribute("TEXCOORD_1", VertexBuffer.UV2Kind);
loadAttribute("TEXCOORD_2", VertexBuffer.UV3Kind);
loadAttribute("TEXCOORD_3", VertexBuffer.UV4Kind);
loadAttribute("TEXCOORD_4", VertexBuffer.UV5Kind);
loadAttribute("TEXCOORD_5", VertexBuffer.UV6Kind);
loadAttribute("JOINTS_0", VertexBuffer.MatricesIndicesKind);
loadAttribute("WEIGHTS_0", VertexBuffer.MatricesWeightsKind);
loadAttribute("COLOR_0", VertexBuffer.ColorKind);
const bufferView = ArrayItem.Get(extensionContext, this._loader.gltf.bufferViews, extension.bufferView);
if (!bufferView._dracoBabylonGeometry) {
bufferView._dracoBabylonGeometry = this._loader.loadBufferViewAsync(`/bufferViews/${bufferView.index}`, bufferView).then(async (data) => {
const dracoDecoder = this.dracoDecoder || DracoDecoder.Default;
const positionAccessor = ArrayItem.TryGet(this._loader.gltf.accessors, primitive.attributes["POSITION"]);
const babylonBoundingInfo = !this._loader.parent.alwaysComputeBoundingBox && !babylonMesh.skeleton && positionAccessor ? LoadBoundingInfoFromPositionAccessor(positionAccessor) : null;
return await dracoDecoder
._decodeMeshToGeometryForGltfAsync(babylonMesh.name, this._loader.babylonScene, data, attributes, normalized, babylonBoundingInfo)
.catch((error) => {
throw new Error(`${context}: ${error.message}`);
});
});
}
return await bufferView._dracoBabylonGeometry;
});
}
}
unregisterGLTFExtension(NAME);
registerGLTFExtension(NAME, true, (loader) => new KHR_draco_mesh_compression(loader));
export { KHR_draco_mesh_compression };
//# sourceMappingURL=KHR_draco_mesh_compression-CMKeOpC4.esm.js.map