gpu-curtains
Version:
gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.
194 lines (193 loc) • 7.34 kB
JavaScript
import { throwWarning } from "../../utils/utils.mjs";
//#region src/extras/loaders/GLTFLoader.ts
const GL = typeof window !== "undefined" && WebGLRenderingContext || { REPEAT: 10497 };
const GLB_MAGIC = 1179937895;
const CHUNK_TYPE = {
JSON: 1313821514,
BIN: 5130562
};
const DEFAULT_TRANSLATION = [
0,
0,
0
];
const DEFAULT_ROTATION = [
0,
0,
0,
1
];
const DEFAULT_SCALE = [
1,
1,
1
];
const absUriRegEx = typeof window !== "undefined" && new RegExp(`^${window.location.protocol}`, "i") || RegExp(`^(http|https):`, "i");
const dataUriRegEx = /^data:/;
/**
* Basic glTF loader class.
*
* Allow to load a glTF from an URI and create the associated {@link ArrayBuffer} and {@link ImageBitmap}.
*
* @example
* ```javascript
* const gltfLoader = new GLTFLoader()
* const gltf = await gltfLoader.loadFromUrl('path/to/model.gltf')
*
* // create a gltfScenesManager from the resulting 'gltf' object
* // assuming 'renderer' is a valid camera or curtains renderer
* const gltfScenesManager = new GLTFScenesManager({ renderer, gltf })
* gltfScenesManager.addMeshes()
* ```
*/
var GLTFLoader = class GLTFLoader {
/**
* {@link GLTFLoader} constructor.
*/
constructor() {
this.gltf = null;
}
/**
* Build the absolute uri of the resource
* @param uri - uri of the resource
* @param baseUrl - base url from which to get all the other assets.
* @returns - absolute uri of the resource
*/
static resolveUri(uri, baseUrl) {
if (!!uri.match(absUriRegEx) || !!uri.match(dataUriRegEx)) return uri;
return baseUrl + uri;
}
/**
* Load a glTF from the given url.
* @param url - url of the glTF.
* @returns - the {@link GPUCurtainsGLTF} created.
*/
async loadFromUrl(url) {
const i = url.lastIndexOf("/");
const baseUrl = i !== 0 ? url.substring(0, i + 1) : "";
const response = await fetch(url);
if (url.endsWith(".gltf")) return this.loadFromJson(await response.json(), baseUrl);
else if (url.endsWith(".glb")) return this.loadFromBinary(await response.arrayBuffer(), baseUrl);
else throw new Error("Unrecognized file extension");
}
/**
* Parse a {@link GLTF.IGLTF | glTF json} and create our {@link gltf} base object.
* @param json - already parsed JSON content.
* @param baseUrl - base url from which to get all the other assets.
* @param binaryChunk - optional binary chunks.
* @returns - {@link gltf} base object.
*/
async loadFromJsonBase(json, baseUrl, binaryChunk = null) {
if (baseUrl === void 0) throw new Error("baseUrl must be specified.");
if (!json.asset) throw new Error("Missing asset description.");
if (json.asset.minVersion !== "2.0" && json.asset.version !== "2.0") throw new Error("Incompatible asset version.");
for (const accessor of json.accessors) {
accessor.byteOffset = accessor.byteOffset ?? 0;
accessor.normalized = accessor.normalized ?? false;
}
for (const bufferView of json.bufferViews) bufferView.byteOffset = bufferView.byteOffset ?? 0;
for (const node of json.nodes) if (!node.matrix) {
node.rotation = node.rotation ?? DEFAULT_ROTATION;
node.scale = node.scale ?? DEFAULT_SCALE;
node.translation = node.translation ?? DEFAULT_TRANSLATION;
}
if (json.samplers) for (const sampler of json.samplers) {
sampler.wrapS = sampler.wrapS ?? GL.REPEAT;
sampler.wrapT = sampler.wrapT ?? GL.REPEAT;
}
const pendingBuffers = [];
if (binaryChunk) pendingBuffers.push(Promise.resolve(binaryChunk));
else for (const index in json.buffers) {
const buffer = json.buffers[index];
const uri = GLTFLoader.resolveUri(buffer.uri, baseUrl);
pendingBuffers[index] = fetch(uri).then((response) => response.arrayBuffer());
}
const pendingImages = [];
const bitmapOptions = {
colorSpaceConversion: "none",
premultiplyAlpha: "none"
};
for (let index = 0; index < json.images?.length || 0; ++index) {
const image = json.images[index];
if (image.uri) if (image.uri.includes(".ktx2") || image.uri.includes(".basis")) throwWarning(`GLTFLoader: Basis/compressed textures not supported. This file could not be loaded: ${image.uri}`);
else if (image.uri.includes(".webp")) pendingImages[index] = new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
createImageBitmap(img, bitmapOptions).then(resolve).catch(reject);
};
img.onerror = reject;
img.src = GLTFLoader.resolveUri(image.uri, baseUrl);
});
else pendingImages[index] = fetch(GLTFLoader.resolveUri(image.uri, baseUrl)).then(async (response) => {
return createImageBitmap(await response.blob(), bitmapOptions);
});
else {
const bufferView = json.bufferViews[image.bufferView];
pendingImages[index] = pendingBuffers[bufferView.buffer].then((buffer) => {
const blob = new Blob([new Uint8Array(buffer, bufferView.byteOffset, bufferView.byteLength)], { type: image.mimeType });
if (image.mimeType === "image/webp") return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = URL.createObjectURL(blob);
img.onload = () => {
createImageBitmap(img, bitmapOptions).then((bitmap) => {
URL.revokeObjectURL(img.src);
resolve(bitmap);
}).catch(reject);
};
img.onerror = (err) => {
URL.revokeObjectURL(img.src);
reject(err);
};
});
else return createImageBitmap(blob, bitmapOptions);
});
}
}
return {
...json,
arrayBuffers: await Promise.all(pendingBuffers),
imagesBitmaps: await Promise.all(pendingImages)
};
}
/**
* Load a glTF from a .glb file.
* @param arrayBuffer - {@link ArrayBuffer} containing the data.
* @param baseUrl - base url from which to get all the other assets.
* @returns - the {@link GPUCurtainsGLTF} created.
*/
async loadFromBinary(arrayBuffer, baseUrl) {
const headerView = new DataView(arrayBuffer, 0, 12);
const magic = headerView.getUint32(0, true);
const version = headerView.getUint32(4, true);
const length = headerView.getUint32(8, true);
if (magic !== GLB_MAGIC) throw new Error("Invalid magic string in binary header.");
if (version !== 2) throw new Error("Incompatible version in binary header.");
const chunks = {};
let chunkOffset = 12;
while (chunkOffset < length) {
const chunkHeaderView = new DataView(arrayBuffer, chunkOffset, 8);
const chunkLength = chunkHeaderView.getUint32(0, true);
const chunkType = chunkHeaderView.getUint32(4, true);
chunks[chunkType] = arrayBuffer.slice(chunkOffset + 8, chunkOffset + 8 + chunkLength);
chunkOffset += chunkLength + 8;
}
if (!chunks[CHUNK_TYPE.JSON]) throw new Error("File contained no json chunk.");
const jsonString = new TextDecoder("utf-8").decode(chunks[CHUNK_TYPE.JSON]);
return this.loadFromJson(JSON.parse(jsonString), baseUrl, chunks[CHUNK_TYPE.BIN]);
}
/**
* Load the glTF json, parse the data and create our {@link GPUCurtainsGLTF} object.
* @param json - already parsed JSON content.
* @param baseUrl - base url from which to get all the other assets.
* @param binaryChunk - optional binary chunks.
* @returns - the {@link GPUCurtainsGLTF} created.
*/
async loadFromJson(json, baseUrl, binaryChunk = null) {
this.gltf = await this.loadFromJsonBase(json, baseUrl, binaryChunk);
return this.gltf;
}
};
//#endregion
export { GLTFLoader };