diff --git a/packages/core/docs/blocks.md b/packages/core/docs/blocks.md index 5939d90..d9b11f1 100644 --- a/packages/core/docs/blocks.md +++ b/packages/core/docs/blocks.md @@ -31,6 +31,16 @@ - Output: `Document` - Uses: glTF Transform document graph - Behavior: Strips materials, assignments, and newly unused textures. +- `CullUnusedBlock` + - Input: `Document` + - Output: the same `Document` + - Uses: glTF Transform `prune()` + - Behavior: Removes unused document resources with library-default behavior. +- `QuantizeBlock` + - Input: `Document` + - Output: the same `Document` + - Uses: glTF Transform `quantize()`; execution-scoped `PlatformIO` + - Behavior: Quantizes mesh attributes with library-default behavior. - `ValidateBlock` - Input: `Document` - Output: the same `Document` diff --git a/packages/core/src/blocks/cullUnusedBlock.ts b/packages/core/src/blocks/cullUnusedBlock.ts new file mode 100644 index 0000000..94d3a37 --- /dev/null +++ b/packages/core/src/blocks/cullUnusedBlock.ts @@ -0,0 +1,22 @@ +import { prune } from "@gltf-transform/functions"; + +import { GltfDocumentType } from "../connectionPoints/gltfDocument"; +import { Block, type BlockOptions } from "./block"; +import { defineBlock } from "./blockDefinition"; + +const CullUnusedBlockDefinition = /* @__PURE__ */ defineBlock({ + type: "transform.cull-unused", + input: GltfDocumentType, + output: GltfDocumentType, + runAsync: async (document) => { + await document.transform(prune()); + return document; + }, +}); + +/** Removes unused document resources with library-default behavior. */ +export class CullUnusedBlock extends Block { + public constructor(options?: BlockOptions) { + super(CullUnusedBlockDefinition, options); + } +} diff --git a/packages/core/src/blocks/quantizeBlock.ts b/packages/core/src/blocks/quantizeBlock.ts new file mode 100644 index 0000000..a5bd3d5 --- /dev/null +++ b/packages/core/src/blocks/quantizeBlock.ts @@ -0,0 +1,28 @@ +import { KHRMeshQuantization } from "@gltf-transform/extensions"; +import { quantize } from "@gltf-transform/functions"; + +import { GltfDocumentType } from "../connectionPoints/gltfDocument"; +import { PlatformIOResource } from "../resources/platformIOResource"; +import { Block, type BlockOptions } from "./block"; +import { defineBlock } from "./blockDefinition"; + +const QuantizeBlockDefinition = /* @__PURE__ */ defineBlock({ + type: "transform.quantize", + input: GltfDocumentType, + output: GltfDocumentType, + resources: { + io: PlatformIOResource, + }, + runAsync: async (document, _config, { io }) => { + io.registerExtensions([KHRMeshQuantization]); + await document.transform(quantize()); + return document; + }, +}); + +/** Quantizes mesh attributes with library-default tuning. */ +export class QuantizeBlock extends Block { + public constructor(options?: BlockOptions) { + super(QuantizeBlockDefinition, options); + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7c1257c..c315186 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,5 @@ export { StripMaterialsBlock, type StripMaterialsBlockOptions } from "./blocks/stripMaterialsBlock"; +export { CullUnusedBlock } from "./blocks/cullUnusedBlock"; export { EncodeDracoBlock } from "./blocks/encodeDracoBlock"; export { EncodeKTX2Block } from "./blocks/encodeKtx2Block"; export { EncodeMeshoptBlock } from "./blocks/encodeMeshoptBlock"; @@ -6,6 +7,7 @@ export { FbxInputBlock } from "./blocks/fbxInputBlock"; export { GltfInputBlock } from "./blocks/gltfInputBlock"; export { GltfOutputBlock, type GltfOutputBlockOptions } from "./blocks/gltfOutputBlock"; export { ObjInputBlock } from "./blocks/objInputBlock"; +export { QuantizeBlock } from "./blocks/quantizeBlock"; export { StlInputBlock } from "./blocks/stlInputBlock"; export { ValidateBlock, type ValidateBlockOptions } from "./blocks/validateBlock"; export { NodeAsset } from "./nodeAsset"; diff --git a/tests/integration/cullUnused.test.ts b/tests/integration/cullUnused.test.ts new file mode 100644 index 0000000..de2569c --- /dev/null +++ b/tests/integration/cullUnused.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from "vitest"; + +import { CullUnusedBlock, GltfInputBlock, GltfOutputBlock, NodeAsset } from "../../packages/core/src/index"; +import { parseGlbAsync } from "../helpers/glb"; +import { generateGltfJson } from "../helpers/gltf"; + +describe("unused resource culling", () => { + it("removes unused resources from connected glTF output", async () => { + const url = "https://example.com/model.gltf"; + vi.stubGlobal( + "fetch", + vi.fn(() => Promise.resolve(new Response(generateGltfWithUnusedMaterialJson()))) + ); + + try { + const source = new GltfInputBlock({ input: url }); + const cullUnused = new CullUnusedBlock(); + const destination = new GltfOutputBlock(); + source.output.connectTo(cullUnused.input); + cullUnused.output.connectTo(destination.input); + + const parsed = await parseGlbAsync(await new NodeAsset({ name: "culled-glb", outputBlock: destination }).executeAsync()); + const primitive = parsed.json.meshes?.[0]?.primitives[0]; + + expect(parsed.json.materials?.map(({ name }) => name)).toEqual(["used"]); + expect(primitive?.material).toBe(0); + expect(primitive?.attributes).toEqual({ NORMAL: 1, POSITION: 0 }); + } finally { + vi.unstubAllGlobals(); + } + }); +}); + +function generateGltfWithUnusedMaterialJson(): string { + const gltf = JSON.parse(generateGltfJson()) as { + materials?: Array<{ name: string }>; + meshes: Array<{ primitives: Array<{ material?: number }> }>; + }; + + gltf.materials = [{ name: "used" }, { name: "unused" }]; + gltf.meshes[0]!.primitives[0]!.material = 0; + return JSON.stringify(gltf); +} diff --git a/tests/integration/quantize.test.ts b/tests/integration/quantize.test.ts new file mode 100644 index 0000000..d4801bf --- /dev/null +++ b/tests/integration/quantize.test.ts @@ -0,0 +1,41 @@ +import { NodeIO } from "@gltf-transform/core"; +import { KHRMeshQuantization } from "@gltf-transform/extensions"; +import { describe, expect, it, vi } from "vitest"; + +import { GltfInputBlock, GltfOutputBlock, NodeAsset, QuantizeBlock } from "../../packages/core/src/index"; +import { parseGlbAsync } from "../helpers/glb"; +import { generateGltfJson } from "../helpers/gltf"; + +describe("mesh quantization", () => { + it("quantizes connected glTF output", async () => { + const url = "https://example.com/model.gltf"; + vi.stubGlobal( + "fetch", + vi.fn(() => Promise.resolve(new Response(generateGltfJson()))) + ); + + try { + const source = new GltfInputBlock({ input: url }); + const quantizer = new QuantizeBlock(); + const destination = new GltfOutputBlock(); + source.output.connectTo(quantizer.input); + quantizer.output.connectTo(destination.input); + + const output = await new NodeAsset({ name: "quantized-glb", outputBlock: destination }).executeAsync(); + const parsed = await parseGlbAsync(output); + const document = await new NodeIO().registerExtensions([KHRMeshQuantization]).readBinary(new Uint8Array(await output.arrayBuffer())); + const primitive = document.getRoot().listMeshes()[0]!.listPrimitives()[0]!; + const position = primitive.getAttribute("POSITION")!; + const normal = primitive.getAttribute("NORMAL")!; + + expect(parsed.json.extensionsUsed).toContain("KHR_mesh_quantization"); + expect(parsed.json.extensionsRequired).toContain("KHR_mesh_quantization"); + expect(position.getArray()).toBeInstanceOf(Int16Array); + expect(position.getNormalized()).toBe(true); + expect(normal.getArray()).toBeInstanceOf(Int16Array); + expect(normal.getNormalized()).toBe(true); + } finally { + vi.unstubAllGlobals(); + } + }); +});