Fest: Sichtbeton, Estrich, Verkleidung, Stahl, Trapezblech, Riffelblech, Verglasung, utility_box_01. Nahflächen: Sichtbeton, Estrich, Verkleidung, Stahl, Sitzschale. Kabel, Kabeldosen, Wandleuchten und Kästen auf 0,25. Der Asset-Test prüft jede Datei aus PLACES gegen ihre eigene Zeile fester Materialien statt gegen die ganze globale Liste, Bump nur beim Hood Yard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
140 lines
7.5 KiB
JavaScript
140 lines
7.5 KiB
JavaScript
import { isAbsolute } from "node:path";
|
||
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_MATERIALS, NEAR_TEXTURE_SIZE, SIMPLIFY_RATIOS, optimizePlace, placeTarget }
|
||
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");
|
||
});
|
||
});
|
||
|
||
describe("placeTarget", () => {
|
||
// Bruch: ein Pfad relativ zum Arbeitsverzeichnis statt zum Projekt — bei einem Aufruf aus einem
|
||
// anderen Ordner (z. B. aus einem Skript) landete die Datei am falschen Ort oder mkdir schlug fehl.
|
||
it("liefert den absoluten Zielpfad unter public/models/places/", () => {
|
||
const target = placeTarget("hood-yard");
|
||
|
||
expect(isAbsolute(target)).toBe(true);
|
||
expect(target.endsWith("public/models/places/hood-yard.glb")).toBe(true);
|
||
});
|
||
|
||
// Bruch: eine ID mit Pfadtrennzeichen oder Großbuchstaben schreibt außerhalb von
|
||
// public/models/places/ oder unter einem Namen, den PLACES so nie referenziert.
|
||
it("lehnt IDs mit Pfadtrennzeichen oder Großbuchstaben ab", () => {
|
||
expect(() => placeTarget("../x")).toThrow();
|
||
expect(() => placeTarget("Hood Yard")).toThrow();
|
||
});
|
||
});
|
||
|
||
describe("Materiallisten der Tribüne", () => {
|
||
// Bruch: Beton, Estrich, Verkleidung, Stahl oder Sitze fielen auf 512 px, obwohl der Spieler direkt
|
||
// davorsteht — oder Dach und Kabinenboden, die man nur von weitem sieht, behielten 768 px.
|
||
it("führt Beton, Estrich, Verkleidung, Stahl und Sitze als Nahflächen, Dach und Kabinenboden nicht", () => {
|
||
for (const name of ["Sichtbeton", "Estrich", "Verkleidung", "Stahl", "Sitzschale"]) expect(NEAR_MATERIALS.has(name)).toBe(true);
|
||
for (const name of ["Trapezblech", "Riffelblech"]) expect(NEAR_MATERIALS.has(name)).toBe(false);
|
||
});
|
||
|
||
// Bruch: Kabel, Kästen und Leuchten blieben bei 64 000 Dreiecken, oder die Vereinfachung träfe Körper,
|
||
// Dach, Geländer oder Sitze (Löcher in Wänden, schiefe Stufenkanten).
|
||
it("vereinfacht nur die Kleinteile der Tribüne, auf 0,25", () => {
|
||
for (const name of ["modular_electric_cables", "modular_electric_cables_boxes", "industrial_wall_lamp",
|
||
"industrial_wall_lamp_glass", "power_box_01", "utility_box_01"]) {
|
||
expect(SIMPLIFY_RATIOS.get(name)).toBe(0.25);
|
||
}
|
||
for (const name of ["Sichtbeton", "Estrich", "Verkleidung", "Stahl", "Trapezblech", "Riffelblech", "Verglasung",
|
||
"Sitzschale"]) {
|
||
expect(SIMPLIFY_RATIOS.has(name)).toBe(false);
|
||
}
|
||
});
|
||
});
|