From b41affd1f381ca6e73ea5a2bc0fdb8f55ad99799 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 27 Nov 2024 21:18:01 +0100 Subject: [PATCH 1/5] Decode meshopt in buffer structure --- src/base/binary/BinaryBufferDataResolver.ts | 152 +++++++++++++++++--- 1 file changed, 135 insertions(+), 17 deletions(-) diff --git a/src/base/binary/BinaryBufferDataResolver.ts b/src/base/binary/BinaryBufferDataResolver.ts index 0ac2e6a..1fd01d5 100644 --- a/src/base/binary/BinaryBufferDataResolver.ts +++ b/src/base/binary/BinaryBufferDataResolver.ts @@ -6,6 +6,8 @@ import { BinaryBufferData } from "./BinaryBufferData"; import { BinaryBufferStructure } from "./BinaryBufferStructure"; import { BinaryDataError } from "./BinaryDataError"; +import { MeshoptDecoder } from "meshoptimizer"; + /** * A class for resolving binary buffer data. * @@ -23,7 +25,7 @@ export class BinaryBufferDataResolver { * * The given `binaryBuffer` will be used as the buffer data * for any buffer that does not have a URI (intended for - * binary subtree files)) + * binary subtree files) * * @param binaryBufferStructure - The `BinaryBufferStructure` * @param binaryBuffer - The optional binary buffer @@ -41,15 +43,11 @@ export class BinaryBufferDataResolver { const buffersData: Buffer[] = []; const buffers = binaryBufferStructure.buffers; if (buffers) { - for (const buffer of buffers) { - if (!defined(buffer.uri)) { - if (!binaryBuffer) { - throw new BinaryDataError( - "Expected a binary buffer, but got undefined" - ); - } - buffersData.push(binaryBuffer); - } else { + for (let b = 0; b < buffers.length; b++) { + const buffer = buffers[b]; + + // If the buffer defines a URI, it will be resolved + if (defined(buffer.uri)) { //console.log("Obtaining buffer data from " + buffer.uri); const bufferData = await resourceResolver.resolveData(buffer.uri); if (!bufferData) { @@ -57,6 +55,30 @@ export class BinaryBufferDataResolver { throw new BinaryDataError(message); } buffersData.push(bufferData); + } else { + // If the buffer does not define a URI, then it might + // be a "fallback" buffer from `EXT_meshopt_compression`. + // In this case, the `EXT_meshopt_compression` extension + // is required, and a dummy buffer with the appropriate + // size will be returned. + const isFallbackBuffer = + BinaryBufferDataResolver.isMeshoptFallbackBuffer( + binaryBufferStructure, + b + ); + if (isFallbackBuffer) { + const fallbackBuffer = Buffer.alloc(buffer.byteLength); + buffersData.push(fallbackBuffer); + } else { + // When the buffer does not have a URI and is not a fallback + // buffer, then it must be the binary buffer + if (!binaryBuffer) { + throw new BinaryDataError( + "Expected a binary buffer, but got undefined" + ); + } + buffersData.push(binaryBuffer); + } } } } @@ -67,18 +89,114 @@ export class BinaryBufferDataResolver { const bufferViews = binaryBufferStructure.bufferViews; if (bufferViews) { for (const bufferView of bufferViews) { - const bufferData = buffersData[bufferView.buffer]; - const start = bufferView.byteOffset; - const end = start + bufferView.byteLength; - const bufferViewData = bufferData.subarray(start, end); - bufferViewsData.push(bufferViewData); + // If the buffer view defines the `EXT_meshopt_compression` + // extension, then decode the meshopt buffer + const extensions = bufferView.extensions ?? {}; + const meshopt = extensions["EXT_meshopt_compression"]; + if (meshopt) { + const compressedBufferData = buffersData[meshopt.buffer]; + const bufferViewData = await BinaryBufferDataResolver.decodeMeshopt( + compressedBufferData, + meshopt + ); + bufferViewsData.push(bufferViewData); + } else { + // Otherwise, just slice out the required part of + // the buffer that the buffer view refers to + const bufferData = buffersData[bufferView.buffer]; + const start = bufferView.byteOffset ?? 0; + const end = start + bufferView.byteLength; + const bufferViewData = bufferData.subarray(start, end); + bufferViewsData.push(bufferViewData); + } } } - const binarybBufferData: BinaryBufferData = { + const binaryBufferData: BinaryBufferData = { buffersData: buffersData, bufferViewsData: bufferViewsData, }; - return binarybBufferData; + return binaryBufferData; + } + + /** + * Decode the meshopt-compressed data for a buffer view that contained + * the given `EXT_meshopt_compression` extension object. + * + * @param compressedBufferData - The buffer containing the compressed + * data that the extension object refers to + * @param meshopt The extension object + * @returns The decoded buffer (view) data + */ + private static async decodeMeshopt( + compressedBufferData: Buffer, + meshopt: any + ): Promise { + const byteOffset = meshopt.byteOffset ?? 0; + const byteLength = meshopt.byteLength; + const byteStride = meshopt.byteStride; + const count = meshopt.count; + const mode = meshopt.mode; + const filter = meshopt.filter ?? "NONE"; + const compressedBufferViewData = compressedBufferData.subarray( + byteOffset, + byteOffset + byteLength + ); + + const uncompressedByteLength = byteStride * count; + const uncompressedBufferViewData = new Uint8Array(uncompressedByteLength); + + await MeshoptDecoder.ready; + MeshoptDecoder.decodeGltfBuffer( + uncompressedBufferViewData, + count, + byteStride, + compressedBufferViewData, + mode, + filter + ); + return Buffer.from(uncompressedBufferViewData); + } + + /** + * Returns whether the given buffer is a "fallback" buffer for the + * `EXT_meshopt_compression` extension. + * + * This is the case when it is either itself marked with + * an `EXT_meshopt_compression` extension object that + * defines `fallback: true`, or when it is referred to by + * a buffer view that defines the `EXT_meshopt_compression` + * extension. + * + * @param binaryBufferStructure - The BinaryBufferStructure + * @param bufferIndex - The buffer index + * @returns Whether the given buffer is a fallback buffer + */ + private static isMeshoptFallbackBuffer( + binaryBufferStructure: BinaryBufferStructure, + bufferIndex: number + ) { + const buffers = binaryBufferStructure.buffers; + const buffer = buffers[bufferIndex]; + if (buffer.extensions) { + const meshopt = buffer.extensions["EXT_meshopt_compression"]; + if (meshopt) { + if (meshopt["fallback"] === true) { + return true; + } + } + } + const bufferViews = binaryBufferStructure.bufferViews; + for (const bufferView of bufferViews) { + if (bufferView.extensions) { + const meshopt = bufferView.extensions["EXT_meshopt_compression"]; + if (meshopt) { + if (bufferView.buffer === bufferIndex) { + return true; + } + } + } + } + return false; } } From 4be0b3c96e965e48eb4b6aec42ec3e20fe46dd0d Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 29 Nov 2024 17:39:34 +0100 Subject: [PATCH 2/5] Cleanup for meshopt in BinaryBufferDataResolver --- src/base/binary/BinaryBufferDataResolver.ts | 232 ++++++++++++++------ 1 file changed, 160 insertions(+), 72 deletions(-) diff --git a/src/base/binary/BinaryBufferDataResolver.ts b/src/base/binary/BinaryBufferDataResolver.ts index 1fd01d5..ca31108 100644 --- a/src/base/binary/BinaryBufferDataResolver.ts +++ b/src/base/binary/BinaryBufferDataResolver.ts @@ -6,6 +6,8 @@ import { BinaryBufferData } from "./BinaryBufferData"; import { BinaryBufferStructure } from "./BinaryBufferStructure"; import { BinaryDataError } from "./BinaryDataError"; +import { BufferView } from "../../structure"; + import { MeshoptDecoder } from "meshoptimizer"; /** @@ -23,9 +25,12 @@ export class BinaryBufferDataResolver { * and returns a `BinaryBufferData` that contains the actual * binary buffer data. * + * If any of the given buffer views uses the `EXT_meshopt_compression` + * glTF extension, then this data will be decompressed. + * * The given `binaryBuffer` will be used as the buffer data - * for any buffer that does not have a URI (intended for - * binary subtree files) + * for any buffer that does not have a URI and that is not + * a meshopt fallback buffer (intended for binary subtree files) * * @param binaryBufferStructure - The `BinaryBufferStructure` * @param binaryBuffer - The optional binary buffer @@ -44,42 +49,13 @@ export class BinaryBufferDataResolver { const buffers = binaryBufferStructure.buffers; if (buffers) { for (let b = 0; b < buffers.length; b++) { - const buffer = buffers[b]; - - // If the buffer defines a URI, it will be resolved - if (defined(buffer.uri)) { - //console.log("Obtaining buffer data from " + buffer.uri); - const bufferData = await resourceResolver.resolveData(buffer.uri); - if (!bufferData) { - const message = `Could not resolve buffer ${buffer.uri}`; - throw new BinaryDataError(message); - } - buffersData.push(bufferData); - } else { - // If the buffer does not define a URI, then it might - // be a "fallback" buffer from `EXT_meshopt_compression`. - // In this case, the `EXT_meshopt_compression` extension - // is required, and a dummy buffer with the appropriate - // size will be returned. - const isFallbackBuffer = - BinaryBufferDataResolver.isMeshoptFallbackBuffer( - binaryBufferStructure, - b - ); - if (isFallbackBuffer) { - const fallbackBuffer = Buffer.alloc(buffer.byteLength); - buffersData.push(fallbackBuffer); - } else { - // When the buffer does not have a URI and is not a fallback - // buffer, then it must be the binary buffer - if (!binaryBuffer) { - throw new BinaryDataError( - "Expected a binary buffer, but got undefined" - ); - } - buffersData.push(binaryBuffer); - } - } + const bufferData = await BinaryBufferDataResolver.resolveBufferData( + binaryBufferStructure, + binaryBuffer, + b, + resourceResolver + ); + buffersData.push(bufferData); } } @@ -89,26 +65,12 @@ export class BinaryBufferDataResolver { const bufferViews = binaryBufferStructure.bufferViews; if (bufferViews) { for (const bufferView of bufferViews) { - // If the buffer view defines the `EXT_meshopt_compression` - // extension, then decode the meshopt buffer - const extensions = bufferView.extensions ?? {}; - const meshopt = extensions["EXT_meshopt_compression"]; - if (meshopt) { - const compressedBufferData = buffersData[meshopt.buffer]; - const bufferViewData = await BinaryBufferDataResolver.decodeMeshopt( - compressedBufferData, - meshopt + const bufferViewData = + await BinaryBufferDataResolver.resolveBufferViewData( + buffersData, + bufferView ); - bufferViewsData.push(bufferViewData); - } else { - // Otherwise, just slice out the required part of - // the buffer that the buffer view refers to - const bufferData = buffersData[bufferView.buffer]; - const start = bufferView.byteOffset ?? 0; - const end = start + bufferView.byteLength; - const bufferViewData = bufferData.subarray(start, end); - bufferViewsData.push(bufferViewData); - } + bufferViewsData.push(bufferViewData); } } @@ -119,42 +81,168 @@ export class BinaryBufferDataResolver { return binaryBufferData; } + /** + * Resolve the data for the specified buffer from the given structure. + * + * This will... + * - resolve the buffer URI if it is present + * - return a zero-filled buffer if the specified buffer is a + * meshopt fallback buffer + * - return the given binary buffer otherwise + * + * @param binaryBufferStructure - Tbe `BinaryBufferStructure` + * @param binaryBuffer - The optional binary buffer + * @param bufferIndex - The index of the buffer to resolve + * @param resourceResolver - The `ResourceResolver` that will be + * used for resolving buffer URIs + * @returns The `Buffer` containing the data for the specified + * buffer. + * @throws BinaryDataError If the buffer data cannot be resolved + */ + private static async resolveBufferData( + binaryBufferStructure: BinaryBufferStructure, + binaryBuffer: Buffer | undefined, + bufferIndex: number, + resourceResolver: ResourceResolver + ): Promise { + const buffer = binaryBufferStructure.buffers[bufferIndex]; + + // If the buffer defines a URI, it will be resolved + if (defined(buffer.uri)) { + //console.log("Obtaining buffer data from " + buffer.uri); + const bufferData = await resourceResolver.resolveData(buffer.uri); + if (!bufferData) { + const message = `Could not resolve buffer ${buffer.uri}`; + throw new BinaryDataError(message); + } + return bufferData; + } + + // If the buffer does not define a URI, then it might + // be a "fallback" buffer from `EXT_meshopt_compression`. + // In this case, a dummy buffer with the appropriate + // size will be returned. Later, when the buffer views + // are processed, slices of this buffer will be filled + // with the data that results from uncompressing the + // meshopt-compressed data + const isFallbackBuffer = BinaryBufferDataResolver.isMeshoptFallbackBuffer( + binaryBufferStructure, + bufferIndex + ); + if (isFallbackBuffer) { + const fallbackBuffer = Buffer.alloc(buffer.byteLength); + return fallbackBuffer; + } + + // When the buffer does not have a URI and is not a fallback + // buffer, then it must be the binary buffer + if (!binaryBuffer) { + throw new BinaryDataError("Expected a binary buffer, but got undefined"); + } + return binaryBuffer; + } + + /** + * Resolve the data for the given buffer view against the given + * buffers data. + * + * This will slice out the data for the buffer view from the + * data that corresponds to its `buffer`. + * + * If the given buffer view uses the `EXT_meshopt_compression` + * glTF extension, then its data will be uncompressed into + * the appropriate slice of the uncompressed buffer, and this + * slice will be returned. + * + * @param buffersData - The buffers data + * @param bufferView - The `BufferView` object + * @returns The `Buffer` for the buffer view + */ + private static async resolveBufferViewData( + buffersData: Buffer[], + bufferView: BufferView + ): Promise { + // If the buffer view defines the `EXT_meshopt_compression` + // extension, then decode the meshopt buffer + const extensions = bufferView.extensions ?? {}; + const meshopt = extensions["EXT_meshopt_compression"]; + if (meshopt) { + const compressedBufferData = buffersData[meshopt.buffer]; + const uncompressedBufferData = buffersData[bufferView.buffer]; + const bufferViewData = await BinaryBufferDataResolver.decodeMeshopt( + compressedBufferData, + uncompressedBufferData, + bufferView, + meshopt + ); + return bufferViewData; + } + + // Otherwise, just slice out the required part of + // the buffer that the buffer view refers to + const bufferData = buffersData[bufferView.buffer]; + const start = bufferView.byteOffset ?? 0; + const end = start + bufferView.byteLength; + const bufferViewData = bufferData.subarray(start, end); + return bufferViewData; + } + /** * Decode the meshopt-compressed data for a buffer view that contained * the given `EXT_meshopt_compression` extension object. * + * The returned uncompressed buffer view will be a slice/subarray + * of the given uncompressed buffer. + * * @param compressedBufferData - The buffer containing the compressed * data that the extension object refers to - * @param meshopt The extension object + * @param uncompressedBufferData - The buffer for the uncompressed + * data. This may be a "fallback" buffer that was created when + * initializing the `buffersData` array. + * @param bufferView - The buffer view that contained the extension + * @param meshopt - The extension object * @returns The decoded buffer (view) data */ private static async decodeMeshopt( compressedBufferData: Buffer, + uncompressedBufferData: Buffer, + bufferView: BufferView, meshopt: any ): Promise { - const byteOffset = meshopt.byteOffset ?? 0; - const byteLength = meshopt.byteLength; - const byteStride = meshopt.byteStride; - const count = meshopt.count; - const mode = meshopt.mode; - const filter = meshopt.filter ?? "NONE"; + // Slice out the compressed buffer view data from the compressed + // buffer, based on the meshopt byte offset and length + const meshoptByteOffset = meshopt.byteOffset ?? 0; + const meshoptByteLength = meshopt.byteLength; const compressedBufferViewData = compressedBufferData.subarray( - byteOffset, - byteOffset + byteLength + meshoptByteOffset, + meshoptByteOffset + meshoptByteLength ); - const uncompressedByteLength = byteStride * count; - const uncompressedBufferViewData = new Uint8Array(uncompressedByteLength); + // Slice out the data for the uncompressed buffer view from the + // uncompressed buffer, based on the buffer view offset and length, + // The buffer is a "fallback buffer" that is initially zero-filled. + const meshoptCount = meshopt.count; + const meshoptByteStride = meshopt.byteStride; + const uncompressedByteLength = meshoptByteStride * meshoptCount; + const uncompressedBufferViewData = uncompressedBufferData.subarray( + bufferView.byteOffset, + bufferView.byteOffset + uncompressedByteLength + ); + // Use the meshopt decoder to fill the uncompressed buffer view + // data from the compressed buffer view data await MeshoptDecoder.ready; + const meshoptMode = meshopt.mode; + const meshoptFilter = meshopt.filter ?? "NONE"; MeshoptDecoder.decodeGltfBuffer( uncompressedBufferViewData, - count, - byteStride, + meshoptCount, + meshoptByteStride, compressedBufferViewData, - mode, - filter + meshoptMode, + meshoptFilter ); + return Buffer.from(uncompressedBufferViewData); } From c39c012af68be1bf3de414e5cc68c9e1dbb46280 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 29 Nov 2024 17:39:56 +0100 Subject: [PATCH 3/5] Specs for BinaryBufferDataResolver --- .../binary/BinaryBufferDataResolverSpec.ts | 98 ++++++++++++++ specs/data/Triangle/TriangleMeshopt.bin | Bin 0 -> 80 bytes specs/data/Triangle/TriangleMeshopt.gltf | 127 ++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 specs/base/binary/BinaryBufferDataResolverSpec.ts create mode 100644 specs/data/Triangle/TriangleMeshopt.bin create mode 100644 specs/data/Triangle/TriangleMeshopt.gltf diff --git a/specs/base/binary/BinaryBufferDataResolverSpec.ts b/specs/base/binary/BinaryBufferDataResolverSpec.ts new file mode 100644 index 0000000..19a9929 --- /dev/null +++ b/specs/base/binary/BinaryBufferDataResolverSpec.ts @@ -0,0 +1,98 @@ +import fs from "fs"; +import path from "path"; + +import { BinaryBufferDataResolver } from "../../../src/base"; +import { ResourceResolvers } from "../../../src/base"; + +import { SpecHelpers } from "../../SpecHelpers"; + +const SPECS_DATA_BASE_DIRECTORY = SpecHelpers.getSpecsDataBaseDirectory(); + +describe("BinaryBufferDataResolver", function () { + it("resolves the data from the buffer structure of a glTF", async function () { + const p = SPECS_DATA_BASE_DIRECTORY + "/Triangle/Triangle.gltf"; + const directory = path.dirname(p); + const resourceResolver = + ResourceResolvers.createFileResourceResolver(directory); + + const gltfBuffer = fs.readFileSync(p); + const gltf = JSON.parse(gltfBuffer.toString()); + const binaryBufferData = await BinaryBufferDataResolver.resolve( + gltf, + undefined, + resourceResolver + ); + + // The indices are unsigned short (16 bit) integer values + const indicesBufferView = binaryBufferData.bufferViewsData[0]; + const indicesArray = new Uint16Array( + indicesBufferView.buffer, + indicesBufferView.byteOffset, + indicesBufferView.byteLength / Uint16Array.BYTES_PER_ELEMENT + ); + const actualIndices = [...indicesArray]; + const expectedIndices = [0, 1, 2]; + + // The positions are float values + const positionsBufferView = binaryBufferData.bufferViewsData[1]; + const positionsArray = new Float32Array( + positionsBufferView.buffer, + positionsBufferView.byteOffset, + positionsBufferView.byteLength / Float32Array.BYTES_PER_ELEMENT + ); + const actualPositions = [...positionsArray]; + // prettier-ignore + const expectedPositions = [ + 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0 + ]; + + expect(actualIndices).toEqual(expectedIndices); + expect(actualPositions).toEqual(expectedPositions); + }); + + it("resolves the data from the buffer structure of a glTF with meshopt compression", async function () { + const p = SPECS_DATA_BASE_DIRECTORY + "/Triangle/TriangleMeshopt.gltf"; + const directory = path.dirname(p); + const resourceResolver = + ResourceResolvers.createFileResourceResolver(directory); + + const gltfBuffer = fs.readFileSync(p); + const gltf = JSON.parse(gltfBuffer.toString()); + const binaryBufferData = await BinaryBufferDataResolver.resolve( + gltf, + undefined, + resourceResolver + ); + + // The indices are unsigned short (16 bit) integer values + const indicesBufferView = binaryBufferData.bufferViewsData[0]; + const indicesArray = new Uint16Array( + indicesBufferView.buffer, + indicesBufferView.byteOffset, + indicesBufferView.byteLength / Uint16Array.BYTES_PER_ELEMENT + ); + const actualIndices = [...indicesArray]; + const expectedIndices = [0, 1, 2]; + + // The positions are ALSO unsigned 16-bit integer values (normalized) + const positionsBufferView = binaryBufferData.bufferViewsData[1]; + const positionsArray = new Uint16Array( + positionsBufferView.buffer, + positionsBufferView.byteOffset, + positionsBufferView.byteLength / Uint16Array.BYTES_PER_ELEMENT + ); + const actualPositions = [...positionsArray]; + // prettier-ignore + const expectedPositions = [ + 32769, 32769, 0, + 0, 32767, 32769, + 0, 0, 32769, + 32767, 0, 0 + ] + + expect(actualIndices).toEqual(expectedIndices); + expect(actualPositions).toEqual(expectedPositions); + }); +}); diff --git a/specs/data/Triangle/TriangleMeshopt.bin b/specs/data/Triangle/TriangleMeshopt.bin new file mode 100644 index 0000000000000000000000000000000000000000..f8f553f7604571067e68327383f66215caca45c8 GIT binary patch literal 80 zcmaF(fuXEDEWKi7TWV*<48}|bAXva?10^3{ literal 0 HcmV?d00001 diff --git a/specs/data/Triangle/TriangleMeshopt.gltf b/specs/data/Triangle/TriangleMeshopt.gltf new file mode 100644 index 0000000..92e71fb --- /dev/null +++ b/specs/data/Triangle/TriangleMeshopt.gltf @@ -0,0 +1,127 @@ +{ + "asset": { + "generator": "glTF-Transform v3.10.1", + "version": "2.0" + }, + "accessors": [ + { + "type": "SCALAR", + "componentType": 5123, + "count": 3, + "normalized": false, + "byteOffset": 0, + "bufferView": 0 + }, + { + "type": "VEC3", + "componentType": 5122, + "count": 3, + "max": [ + 32767, + 32767, + 0 + ], + "min": [ + -32767, + -32767, + 0 + ], + "normalized": true, + "byteOffset": 0, + "bufferView": 1 + } + ], + "bufferViews": [ + { + "buffer": 1, + "byteOffset": 0, + "byteLength": 6, + "target": 34963, + "extensions": { + "EXT_meshopt_compression": { + "buffer": 0, + "byteOffset": 0, + "byteLength": 18, + "mode": "TRIANGLES", + "byteStride": 2, + "count": 3 + } + } + }, + { + "buffer": 1, + "byteOffset": 8, + "byteLength": 24, + "target": 34962, + "byteStride": 8, + "extensions": { + "EXT_meshopt_compression": { + "buffer": 0, + "byteOffset": 20, + "byteLength": 60, + "mode": "ATTRIBUTES", + "byteStride": 8, + "count": 3 + } + } + } + ], + "buffers": [ + { + "uri": "TriangleMeshopt.bin", + "byteLength": 80 + }, + { + "byteLength": 32, + "extensions": { + "EXT_meshopt_compression": { + "fallback": true + } + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 1 + }, + "mode": 4, + "indices": 0 + } + ] + } + ], + "nodes": [ + { + "translation": [ + 0.5, + 0.5, + 0 + ], + "scale": [ + 0.5, + 0.5, + 0.5 + ], + "mesh": 0 + } + ], + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "scene": 0, + "extensionsUsed": [ + "KHR_mesh_quantization", + "EXT_meshopt_compression" + ], + "extensionsRequired": [ + "KHR_mesh_quantization", + "EXT_meshopt_compression" + ] +} \ No newline at end of file From a43766ed3583d2ad129056d05fbb3c713386c91e Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 29 Nov 2024 18:03:15 +0100 Subject: [PATCH 4/5] Handle missing byteOffset in buffer view --- src/base/binary/BinaryBufferDataResolver.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/base/binary/BinaryBufferDataResolver.ts b/src/base/binary/BinaryBufferDataResolver.ts index ca31108..92dd675 100644 --- a/src/base/binary/BinaryBufferDataResolver.ts +++ b/src/base/binary/BinaryBufferDataResolver.ts @@ -224,9 +224,10 @@ export class BinaryBufferDataResolver { const meshoptCount = meshopt.count; const meshoptByteStride = meshopt.byteStride; const uncompressedByteLength = meshoptByteStride * meshoptCount; + const bufferViewByteOffset = bufferView.byteOffset ?? 0; const uncompressedBufferViewData = uncompressedBufferData.subarray( - bufferView.byteOffset, - bufferView.byteOffset + uncompressedByteLength + bufferViewByteOffset, + bufferViewByteOffset + uncompressedByteLength ); // Use the meshopt decoder to fill the uncompressed buffer view From 1eec43944eb0a31983116f3db29e2d59534ecef3 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 29 Nov 2024 18:21:19 +0100 Subject: [PATCH 5/5] Add spec for byteOffset default handling --- .../binary/BinaryBufferDataResolverSpec.ts | 45 +++++++ .../Triangle/TriangleMeshoptNoByteOffset.gltf | 123 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 specs/data/Triangle/TriangleMeshoptNoByteOffset.gltf diff --git a/specs/base/binary/BinaryBufferDataResolverSpec.ts b/specs/base/binary/BinaryBufferDataResolverSpec.ts index 19a9929..ca406d7 100644 --- a/specs/base/binary/BinaryBufferDataResolverSpec.ts +++ b/specs/base/binary/BinaryBufferDataResolverSpec.ts @@ -95,4 +95,49 @@ describe("BinaryBufferDataResolver", function () { expect(actualIndices).toEqual(expectedIndices); expect(actualPositions).toEqual(expectedPositions); }); + + it("resolves the data from the buffer structure of a glTF without default byte offsets with meshopt compression", async function () { + const p = + SPECS_DATA_BASE_DIRECTORY + "/Triangle/TriangleMeshoptNoByteOffset.gltf"; + const directory = path.dirname(p); + const resourceResolver = + ResourceResolvers.createFileResourceResolver(directory); + + const gltfBuffer = fs.readFileSync(p); + const gltf = JSON.parse(gltfBuffer.toString()); + const binaryBufferData = await BinaryBufferDataResolver.resolve( + gltf, + undefined, + resourceResolver + ); + + // The indices are unsigned short (16 bit) integer values + const indicesBufferView = binaryBufferData.bufferViewsData[0]; + const indicesArray = new Uint16Array( + indicesBufferView.buffer, + indicesBufferView.byteOffset, + indicesBufferView.byteLength / Uint16Array.BYTES_PER_ELEMENT + ); + const actualIndices = [...indicesArray]; + const expectedIndices = [0, 1, 2]; + + // The positions are ALSO unsigned 16-bit integer values (normalized) + const positionsBufferView = binaryBufferData.bufferViewsData[1]; + const positionsArray = new Uint16Array( + positionsBufferView.buffer, + positionsBufferView.byteOffset, + positionsBufferView.byteLength / Uint16Array.BYTES_PER_ELEMENT + ); + const actualPositions = [...positionsArray]; + // prettier-ignore + const expectedPositions = [ + 32769, 32769, 0, + 0, 32767, 32769, + 0, 0, 32769, + 32767, 0, 0 + ] + + expect(actualIndices).toEqual(expectedIndices); + expect(actualPositions).toEqual(expectedPositions); + }); }); diff --git a/specs/data/Triangle/TriangleMeshoptNoByteOffset.gltf b/specs/data/Triangle/TriangleMeshoptNoByteOffset.gltf new file mode 100644 index 0000000..b043dc4 --- /dev/null +++ b/specs/data/Triangle/TriangleMeshoptNoByteOffset.gltf @@ -0,0 +1,123 @@ +{ + "asset": { + "generator": "glTF-Transform v3.10.1", + "version": "2.0" + }, + "accessors": [ + { + "type": "SCALAR", + "componentType": 5123, + "count": 3, + "normalized": false, + "bufferView": 0 + }, + { + "type": "VEC3", + "componentType": 5122, + "count": 3, + "max": [ + 32767, + 32767, + 0 + ], + "min": [ + -32767, + -32767, + 0 + ], + "normalized": true, + "bufferView": 1 + } + ], + "bufferViews": [ + { + "buffer": 1, + "byteLength": 6, + "target": 34963, + "extensions": { + "EXT_meshopt_compression": { + "buffer": 0, + "byteLength": 18, + "mode": "TRIANGLES", + "byteStride": 2, + "count": 3 + } + } + }, + { + "buffer": 1, + "byteOffset": 8, + "byteLength": 24, + "target": 34962, + "byteStride": 8, + "extensions": { + "EXT_meshopt_compression": { + "buffer": 0, + "byteOffset": 20, + "byteLength": 60, + "mode": "ATTRIBUTES", + "byteStride": 8, + "count": 3 + } + } + } + ], + "buffers": [ + { + "uri": "TriangleMeshopt.bin", + "byteLength": 80 + }, + { + "byteLength": 32, + "extensions": { + "EXT_meshopt_compression": { + "fallback": true + } + } + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 1 + }, + "mode": 4, + "indices": 0 + } + ] + } + ], + "nodes": [ + { + "translation": [ + 0.5, + 0.5, + 0 + ], + "scale": [ + 0.5, + 0.5, + 0.5 + ], + "mesh": 0 + } + ], + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "scene": 0, + "extensionsUsed": [ + "KHR_mesh_quantization", + "EXT_meshopt_compression" + ], + "extensionsRequired": [ + "KHR_mesh_quantization", + "EXT_meshopt_compression" + ] +} \ No newline at end of file