@loaders.gl/gltf
Version:
Framework-independent loader for the glTF format
132 lines • 6.16 kB
JavaScript
// https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression
// Only TRIANGLES: 0x0004 and TRIANGLE_STRIP: 0x0005 are supported
/* eslint-disable camelcase */
import { sliceArrayBuffer, parseFromContext } from '@loaders.gl/loader-utils';
import { DracoLoader } from '@loaders.gl/draco';
import { GLTFScenegraph } from "../api/gltf-scenegraph.js";
import { getGLTFAccessors, getGLTFAccessor } from "../gltf-utils/gltf-attribute-utils.js";
const KHR_DRACO_MESH_COMPRESSION = 'KHR_draco_mesh_compression';
/** Extension name */
export const name = KHR_DRACO_MESH_COMPRESSION;
export function preprocess(gltfData, options, context) {
const scenegraph = new GLTFScenegraph(gltfData);
for (const primitive of makeMeshPrimitiveIterator(scenegraph)) {
if (scenegraph.getObjectExtension(primitive, KHR_DRACO_MESH_COMPRESSION)) {
// TODO - Remove fallback accessors to make sure we don't load unnecessary buffers
}
}
}
export async function decode(gltfData, options, context) {
if (!options?.gltf?.decompressMeshes) {
return;
}
const scenegraph = new GLTFScenegraph(gltfData);
const promises = [];
for (const primitive of makeMeshPrimitiveIterator(scenegraph)) {
if (scenegraph.getObjectExtension(primitive, KHR_DRACO_MESH_COMPRESSION)) {
promises.push(decompressPrimitive(scenegraph, primitive, options, context));
}
}
// Decompress meshes in parallel
await Promise.all(promises);
// We have now decompressed all primitives, so remove the top-level extension
scenegraph.removeExtension(KHR_DRACO_MESH_COMPRESSION);
}
export function encode(gltfData, options = {}) {
const scenegraph = new GLTFScenegraph(gltfData);
for (const mesh of scenegraph.json.meshes || []) {
// eslint-disable-next-line camelcase
// @ts-ignore
compressMesh(mesh, options);
// NOTE: Only add the extension if something was actually compressed
scenegraph.addRequiredExtension(KHR_DRACO_MESH_COMPRESSION);
}
}
// DECODE
// Unpacks one mesh primitive and removes the extension from the primitive
// DracoDecoder needs to be imported and registered by app
// Returns: Promise that resolves when all pending draco decoder jobs for this mesh complete
// TODO - Implement fallback behavior per KHR_DRACO_MESH_COMPRESSION spec
async function decompressPrimitive(scenegraph, primitive, options, context) {
const dracoExtension = scenegraph.getObjectExtension(primitive, KHR_DRACO_MESH_COMPRESSION);
if (!dracoExtension) {
return;
}
const buffer = scenegraph.getTypedArrayForBufferView(dracoExtension.bufferView);
// TODO - parse does not yet deal well with byte offsets embedded in typed arrays. Copy buffer
// TODO - remove when `parse` is fixed to handle `byteOffset`s
const bufferCopy = sliceArrayBuffer(buffer.buffer, buffer.byteOffset); // , buffer.byteLength);
const dracoOptions = { ...options };
// TODO - remove hack: The entire tileset might be included, too expensive to serialize
delete dracoOptions['3d-tiles'];
const decodedData = await parseFromContext(bufferCopy, DracoLoader, dracoOptions, context);
const decodedAttributes = getGLTFAccessors(decodedData.attributes);
// Restore min/max values
for (const [attributeName, decodedAttribute] of Object.entries(decodedAttributes)) {
if (attributeName in primitive.attributes) {
const accessorIndex = primitive.attributes[attributeName];
const accessor = scenegraph.getAccessor(accessorIndex);
if (accessor?.min && accessor?.max) {
decodedAttribute.min = accessor.min;
decodedAttribute.max = accessor.max;
}
}
}
// @ts-ignore
primitive.attributes = decodedAttributes;
if (decodedData.indices) {
// @ts-ignore
primitive.indices = getGLTFAccessor(decodedData.indices);
}
// Extension has been processed, delete it
scenegraph.removeObjectExtension(primitive, KHR_DRACO_MESH_COMPRESSION);
checkPrimitive(primitive);
}
// ENCODE
// eslint-disable-next-line max-len
// Only TRIANGLES: 0x0004 and TRIANGLE_STRIP: 0x0005 are supported
function compressMesh(attributes, indices, mode = 4, options, context) {
if (!options.DracoWriter) {
throw new Error('options.gltf.DracoWriter not provided');
}
// TODO - use DracoWriter using encode w/ registered DracoWriter...
const compressedData = options.DracoWriter.encodeSync({ attributes });
// Draco compression may change the order and number of vertices in a mesh.
// To satisfy the requirement that accessors properties be correct for both
// compressed and uncompressed data, generators should create uncompressed
// attributes and indices using data that has been decompressed from the Draco buffer,
// rather than the original source data.
// @ts-ignore TODO this needs to be fixed
const decodedData = context?.parseSync?.({ attributes });
const fauxAccessors = options._addFauxAttributes(decodedData.attributes);
const bufferViewIndex = options.addBufferView(compressedData);
const glTFMesh = {
primitives: [
{
attributes: fauxAccessors, // TODO - verify with spec
mode, // GL.POINTS
extensions: {
[KHR_DRACO_MESH_COMPRESSION]: {
bufferView: bufferViewIndex,
attributes: fauxAccessors // TODO - verify with spec
}
}
}
]
};
return glTFMesh;
}
// UTILS
function checkPrimitive(primitive) {
if (!primitive.attributes && Object.keys(primitive.attributes).length > 0) {
throw new Error('glTF: Empty primitive detected: Draco decompression failure?');
}
}
function* makeMeshPrimitiveIterator(scenegraph) {
for (const mesh of scenegraph.json.meshes || []) {
for (const primitive of mesh.primitives) {
yield primitive;
}
}
}
//# sourceMappingURL=KHR_draco_mesh_compression.js.map