feat(assets): Aufbereitung für Orte mit Texturgrößen je Material und Vereinfachung

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Haiku 4.5 committed 2026-09-14 10:49:31 +02:00
1 parent 0ebe8adb8b
commit 6c291c4106
2 files changed
+239

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
import { beforeAll, describe, expect, it } from "vitest";
import { Document } from "@gltf-transform/core";
import sharp from "sharp";
import { EXT_MATERIALS_BUMP, EXTMaterialsBump } from "../tools/assets/bumpExtension.mjs";
import { FAR_TEXTURE_SIZE, NEAR_TEXTURE_SIZE, optimizePlace } from "../tools/assets/places.mjs";
/** 1100 px große Streifen. seed macht die Bilder verschieden, sonst legt dedup() sie zusammen. */
function stripes(seed) {
const size = 1100;
const data = Buffer.alloc(size * size * 3);
for (let i = 0; i < data.length; i++) data[i] = (Math.floor(i / 3) % size) * seed & 0xff;
return sharp(data, { raw: { width: size, height: size, channels: 3 } }).png().toBuffer();
}
/** n × n Quadrate in der Ebene (2·n² Dreiecke), indiziert. */
function grid(document, n) {
const positions = [];
const indices = [];
for (let z = 0; z <= n; z++) for (let x = 0; x <= n; x++) positions.push(x, 0, z);
for (let z = 0; z < n; z++) {
for (let x = 0; x < n; x++) {
const a = z * (n + 1) + x, b = a + 1, c = a + n + 1, d = c + 1;
indices.push(a, c, b, b, c, d);
}
}
return document.createPrimitive()
.setAttribute("POSITION", document.createAccessor().setType("VEC3").setArray(new Float32Array(positions)))
.setIndices(document.createAccessor().setType("SCALAR").setArray(new Uint32Array(indices)));
}
/** Ein Container (Nahfläche mit Bump) und ein Grasstück (Pflanze mit Normal-Map), je ein Knoten. */
async function sampleDocument() {
const document = new Document();
document.createBuffer();
const bumpExtension = document.createExtension(EXTMaterialsBump);
const [near, nearBump, far, farNormal] = await Promise.all([1, 3, 5, 7].map(stripes));
const texture = (name, image) => document.createTexture(name).setImage(image).setMimeType("image/png");
const container = document.createMaterial("cont_a_side").setBaseColorTexture(texture("near", near));
container.setExtension(EXT_MATERIALS_BUMP,
bumpExtension.createBump().setBumpFactor(2.2).setBumpTexture(texture("nearBump", nearBump)));
const grass = document.createMaterial("grass_medium_01")
.setBaseColorTexture(texture("far", far))
.setNormalTexture(texture("farNormal", farNormal));
const scene = document.createScene();
scene.addChild(document.createNode("container").setMesh(document.createMesh().addPrimitive(grid(document, 1).setMaterial(container))));
scene.addChild(document.createNode("grass").setMesh(document.createMesh().addPrimitive(grid(document, 40).setMaterial(grass))));
return { document, container, grass };
}
const trianglesOf = (document, name) => document.getRoot().listMeshes()
.flatMap((mesh) => mesh.listPrimitives())
.filter((primitive) => primitive.getMaterial().getName() === name)
.reduce((sum, primitive) => sum + primitive.getIndices().getCount() / 3, 0);
describe("optimizePlace", () => {
let sample;
beforeAll(async () => {
sample = await sampleDocument();
await optimizePlace(sample.document);
}, 60000);
// Bruch: Nahflächen so unscharf wie Pflanzen, Größen vertauscht, oder die Bump-Map zählt nicht
// zu ihrem Material (sie hängt über die Erweiterung dran, nicht direkt am Material).
it("verkleinert Texturen je nach Material auf 1024 oder 512 px und wandelt sie in WebP", () => {
const { document, container, grass } = sample;
const bumpTexture = container.getExtension(EXT_MATERIALS_BUMP).getBumpTexture();
expect(container.getBaseColorTexture().getSize()).toEqual([NEAR_TEXTURE_SIZE, NEAR_TEXTURE_SIZE]);
expect(bumpTexture.getSize()).toEqual([NEAR_TEXTURE_SIZE, NEAR_TEXTURE_SIZE]);
expect(grass.getBaseColorTexture().getSize()).toEqual([FAR_TEXTURE_SIZE, FAR_TEXTURE_SIZE]);
expect(grass.getNormalTexture().getSize()).toEqual([FAR_TEXTURE_SIZE, FAR_TEXTURE_SIZE]);
for (const texture of document.getRoot().listTextures()) expect(texture.getMimeType()).toBe("image/webp");
});
// Bruch: die Vereinfachung trifft Container und Schuppen (Löcher in Wänden) oder lässt Pflanzen
// unberührt (die Köcherbäume blieben bei 2,2 Mio. Dreiecken).
it("vereinfacht nur Pflanzen und Kleinteile und behält die Materialnamen", () => {
const { document } = sample;
expect(trianglesOf(document, "cont_a_side")).toBe(2);
expect(trianglesOf(document, "grass_medium_01")).toBeGreaterThan(0);
expect(trianglesOf(document, "grass_medium_01")).toBeLessThanOrEqual(3200 * 0.5);
});
// Bruch: das Wellblech geht verloren, die Geometrie bleibt unkomprimiert, oder es entstehen
// GPU-Instanzen, deren Matrizen die Kollision nicht liest.
it("erhält die Bump-Map, komprimiert mit meshopt und legt keine Instanzen an", () => {
const { document, container } = sample;
const used = document.getRoot().listExtensionsUsed().map((extension) => extension.extensionName);
expect(container.getExtension(EXT_MATERIALS_BUMP).getBumpFactor()).toBe(2.2);
expect(used).toEqual(expect.arrayContaining([EXT_MATERIALS_BUMP, "EXT_meshopt_compression", "EXT_texture_webp"]));
expect(used).not.toContain("EXT_mesh_gpu_instancing");
});
});