feat(assets): EXT_materials_bump für gltf-transform

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-14 10:39:39 +02:00
1 parent 3321a029fa
commit 0ebe8adb8b
4 files changed
+997 -5

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { Document, NodeIO } from "@gltf-transform/core";
import { KHRTextureTransform } from "@gltf-transform/extensions";
import { dedup, prune } from "@gltf-transform/functions";
import sharp from "sharp";
import { EXT_MATERIALS_BUMP, EXTMaterialsBump } from "../tools/assets/bumpExtension.mjs";
/** 8 × 8 px mit wechselnden Werten — prune() ersetzt einfarbige Texturen sonst durch Faktoren. */
function pattern() {
const data = Buffer.alloc(8 * 8 * 3);
for (let i = 0; i < data.length; i++) data[i] = (i * 37) & 0xff;
return sharp(data, { raw: { width: 8, height: 8, channels: 3 } }).png().toBuffer();
}
/** Ein Dreieck mit dem Material cont_a_side, Bump-Stärke 2,2 und Wellenteilung [3.2, 1]. */
async function bumpDocument() {
const document = new Document();
document.createBuffer();
const bumpExtension = document.createExtension(EXTMaterialsBump);
const transformExtension = document.createExtension(KHRTextureTransform);
const texture = document.createTexture("wellblech").setImage(await pattern()).setMimeType("image/png");
const bump = bumpExtension.createBump().setBumpFactor(2.2).setBumpTexture(texture);
bump.getBumpTextureInfo().setExtension("KHR_texture_transform", transformExtension.createTransform().setScale([3.2, 1]));
const material = document.createMaterial("cont_a_side").setExtension(EXT_MATERIALS_BUMP, bump);
const positions = document.createAccessor().setType("VEC3").setArray(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]));
const primitive = document.createPrimitive().setMaterial(material).setAttribute("POSITION", positions);
document.createScene().addChild(document.createNode().setMesh(document.createMesh().addPrimitive(primitive)));
return document;
}
/** JSON-Teil einer .glb: 12 Byte Kopf, dann Länge und Typ des ersten Blocks, dann das JSON. */
function glbJson(glb) {
const view = new DataView(glb.buffer, glb.byteOffset, glb.byteLength);
return JSON.parse(new TextDecoder().decode(glb.subarray(20, 20 + view.getUint32(12, true))));
}
describe("EXT_materials_bump", () => {
const io = () => new NodeIO().registerExtensions([EXTMaterialsBump, KHRTextureTransform]);
// Bruch: @gltf-transform verwirft die Erweiterung (Wellblech flach), prune entfernt die Textur als
// unbenutzt, oder die Wellenteilung an der bumpTexture geht verloren (Wellen 3,2-mal zu breit).
it("übersteht dedup, prune, Schreiben und Einlesen mit Stärke, Textur und Teilung", async () => {
const document = await bumpDocument();
await document.transform(dedup(), prune());
const read = await io().readBinary(await io().writeBinary(document));
const bump = read.getRoot().listMaterials()[0].getExtension(EXT_MATERIALS_BUMP);
expect(bump.getBumpFactor()).toBe(2.2);
expect(bump.getBumpTexture().getSize()).toEqual([8, 8]);
expect(bump.getBumpTextureInfo().getExtension("KHR_texture_transform").getScale()).toEqual([3.2, 1]);
});
// Bruch: Feldnamen oder Aufbau weichen von der Spezifikation ab — three.js (GLTFLoader) liest
// genau materials[i].extensions.EXT_materials_bump.bumpFactor und .bumpTexture.index.
it("schreibt das JSON so, wie three.js es liest", async () => {
const json = glbJson(await io().writeBinary(await bumpDocument()));
expect(json.extensionsUsed).toContain(EXT_MATERIALS_BUMP);
const def = json.materials[0].extensions[EXT_MATERIALS_BUMP];
expect(def.bumpFactor).toBe(2.2);
expect(json.textures[def.bumpTexture.index]).toBeDefined();
expect(def.bumpTexture.extensions.KHR_texture_transform.scale).toEqual([3.2, 1]);
});
});