Files
gta6/tests/placesPipeline.test.js
T
knsoandClaude Sonnet 5 ae0f6106bb feat(world): Materialrollen der Tribüne, Asset-Test je Ort
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>
2026-09-14 20:24:09 +02:00

140 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
});
});