From ae0f6106bbcfe9b49bcfae72e76cf2f7dcdbbacd Mon Sep 17 00:00:00 2001 From: KNSO Date: Mon, 14 Sep 2026 20:24:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(world):=20Materialrollen=20der=20Trib?= =?UTF-8?q?=C3=BCne,=20Asset-Test=20je=20Ort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/world/places.js | 5 ++- tests/places.test.js | 13 ++++++++ tests/placesAsset.test.js | 59 +++++++++++++++++++++++++++--------- tests/placesPipeline.test.js | 25 ++++++++++++++- tools/assets/places.mjs | 13 +++++--- 5 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/world/places.js b/src/world/places.js index 2fdf163..93a913e 100644 --- a/src/world/places.js +++ b/src/world/places.js @@ -34,7 +34,7 @@ export const PLACES = [ }, ]; -/** Materialien, die kollidieren. Alles andere — Gras, Blätter, Büsche, Kleinteile — läuft man durch. */ +/** Materialien, die kollidieren. Alles andere — Gras, Blätter, Büsche, Sitzschalen, Kleinteile — läuft man durch. */ export const SOLID_MATERIALS = [ // Boden "gravel", "asphalt", "curbAsphalt", "padAsphalt", "puddleAsphalt", "cobble", "grass", "concrete", @@ -49,6 +49,9 @@ export const SOLID_MATERIALS = [ "modular_electricity_poles", "barrel_03", "plastic_container", // Bäume "quiver_tree_01_trunk", "quiver_tree_02", + // Tribüne: Betonkörper und Tunnel, Verkleidung, Stahl (Geländer, Dachtragwerk, Rinne), Dach, Kabinenboden, + // Glas der Pressekabine, Anschlusskästen am Boden + "Sichtbeton", "Estrich", "Verkleidung", "Stahl", "Trapezblech", "Riffelblech", "Verglasung", "utility_box_01", ]; const SOLID = new Set(SOLID_MATERIALS); diff --git a/tests/places.test.js b/tests/places.test.js index a891f7f..22d92da 100644 --- a/tests/places.test.js +++ b/tests/places.test.js @@ -61,6 +61,19 @@ describe("isSolidMaterial", () => { } }); + // Bruch: Die Tribüne wäre an Beton, Stahl, Dach, Glas oder den Anschlusskästen durchlässig, oder man + // bliebe an Sitzschalen, Kabeln und Wandleuchten hängen (zwischen den Sitzreihen sind nur 38 cm). + it("lässt Beton, Stahl, Dach, Glas und Anschlusskästen der Tribüne kollidieren, Sitze, Kabel und Leuchten nicht", () => { + for (const name of ["Sichtbeton", "Estrich", "Verkleidung", "Stahl", "Trapezblech", "Riffelblech", "Verglasung", + "utility_box_01"]) { + expect(isSolidMaterial(name)).toBe(true); + } + for (const name of ["Sitzschale", "modular_electric_cables", "modular_electric_cables_boxes", "industrial_wall_lamp", + "industrial_wall_lamp_glass", "power_box_01"]) { + expect(isSolidMaterial(name)).toBe(false); + } + }); + it("kennt jedes Material der Liste", () => { expect(SOLID_MATERIALS.every(isSolidMaterial)).toBe(true); }); diff --git a/tests/placesAsset.test.js b/tests/placesAsset.test.js index 8df9023..42bb6e6 100644 --- a/tests/placesAsset.test.js +++ b/tests/placesAsset.test.js @@ -8,11 +8,34 @@ import { MeshoptDecoder } from "meshoptimizer"; import { PLACES, SOLID_MATERIALS } from "../src/world/places.js"; import { EXT_MATERIALS_BUMP, EXTMaterialsBump } from "../tools/assets/bumpExtension.mjs"; -const place = PLACES.find((entry) => entry.id === "hood-yard"); -const file = fileURLToPath(new URL(`../public/${place.url.slice(2)}`, import.meta.url)); -const CONTAINERS = ["cont_a_side", "cont_a_end", "cont_b_side", "cont_b_end", "cont_c_side", "cont_c_end"]; +// Feste Materialien je Ort. SOLID_MATERIALS gilt für alle Orte zusammen, aber keine Datei enthält alle +// Namen — geprüft wird deshalb jede Datei gegen ihre eigene Zeile. +const SOLID_BY_PLACE = { + "hood-yard": [ + "gravel", "asphalt", "curbAsphalt", "padAsphalt", "puddleAsphalt", "cobble", "grass", "concrete", + "cont_a_side", "cont_a_end", "cont_b_side", "cont_b_end", "cont_c_side", "cont_c_end", "contFrame", + "ironPlain", "ironTeal", "ironBlue", "galv", "rust", "darkTrim", "osb", "osbB", + "rollershutter_window_01", "rollershutter_window_01_graffiti", + "chainlink_post", "chainlink_wire", + "modular_electricity_poles", "barrel_03", "plastic_container", + "quiver_tree_01_trunk", "quiver_tree_02", + ], + tribuene: ["Sichtbeton", "Estrich", "Verkleidung", "Stahl", "Trapezblech", "Riffelblech", "Verglasung", "utility_box_01"], +}; -describe("hood-yard.glb", () => { +// Materialien, deren Relief nur aus EXT_materials_bump kommt. Die Tribüne hat keine Bump-Maps. +const BUMP_BY_PLACE = { + "hood-yard": ["cont_a_side", "cont_a_end", "cont_b_side", "cont_b_end", "cont_c_side", "cont_c_end"], +}; + +// Bruch: ein festes Material steht in keiner Zeile — die Datei seines Orts würde nie darauf geprüft. +it("verteilt SOLID_MATERIALS vollständig auf die Orte", () => { + expect(Object.values(SOLID_BY_PLACE).flat().sort()).toEqual([...SOLID_MATERIALS].sort()); +}); + +describe.each(PLACES.map((place) => [place.id, place]))("%s.glb", (id, place) => { + const file = fileURLToPath(new URL(`../public/${place.url.slice(2)}`, import.meta.url)); + const bump = BUMP_BY_PLACE[id] ?? []; let document; beforeAll(async () => { await MeshoptDecoder.ready; @@ -22,17 +45,16 @@ describe("hood-yard.glb", () => { document = await io.read(file); }, 30000); - // Bruch: Aufbereitung übersprungen oder falsch eingestellt — die Quelle hatte 653 MB. + // Bruch: Aufbereitung übersprungen oder falsch eingestellt — die Quelle des Hood Yard hatte 653 MB. it("ist kleiner als 30 MB", () => { expect(statSync(file).size).toBeLessThan(30e6); }); - // Bruch: das Wellblech ist weg, die Geometrie unkomprimiert, oder GPU-Instanzen, deren Matrizen die - // Kollision des Controllers nicht liest. - it("nutzt meshopt und WebP, behält EXT_materials_bump und hat keine Instanzen", () => { + // Bruch: Geometrie unkomprimiert oder GPU-Instanzen, deren Matrizen die Kollision des Controllers nicht liest. + it("nutzt meshopt und WebP und hat keine Instanzen", () => { const used = document.getRoot().listExtensionsUsed().map((extension) => extension.extensionName); - expect(used).toEqual(expect.arrayContaining(["EXT_meshopt_compression", "EXT_texture_webp", EXT_MATERIALS_BUMP])); + expect(used).toEqual(expect.arrayContaining(["EXT_meshopt_compression", "EXT_texture_webp"])); expect(used).not.toContain("EXT_mesh_gpu_instancing"); }); @@ -43,18 +65,25 @@ describe("hood-yard.glb", () => { } }); - it("gibt allen sechs Container-Materialien ihre Bump-Textur", () => { + // Bruch: die Aufbereitung verwirft EXT_materials_bump (wie die gltf-transform-CLI) — das Wellblech der + // Container wäre flach. + it.runIf(bump.length > 0)("gibt den Bump-Materialien ihre Bump-Textur", () => { + const used = document.getRoot().listExtensionsUsed().map((extension) => extension.extensionName); const materials = new Map(document.getRoot().listMaterials().map((material) => [material.getName(), material])); - for (const name of CONTAINERS) { + + expect(used).toContain(EXT_MATERIALS_BUMP); + for (const name of bump) { expect(materials.get(name)?.getExtension(EXT_MATERIALS_BUMP)?.getBumpTexture()).toBeTruthy(); } }); - // Bruch: dedup oder ein neues Werkzeug benennt ein Material um — der Hof wäre dort ohne Warnung - // durchlässig, weil die Kollision nach Namen auswählt. - it("enthält jedes feste Material der Kollisionsliste", () => { + // Bruch: dedup oder ein neues Werkzeug benennt ein Material um — der Ort wäre dort ohne Warnung + // durchlässig, weil die Kollision nach Namen auswählt. Oder der Ort hat keine Zeile in SOLID_BY_PLACE. + it("enthält jedes feste Material seines Orts", () => { const names = document.getRoot().listMaterials().map((material) => material.getName()); - expect(SOLID_MATERIALS.filter((name) => !names.includes(name))).toEqual([]); + + expect(SOLID_BY_PLACE[id]).toBeDefined(); + expect(SOLID_BY_PLACE[id].filter((name) => !names.includes(name))).toEqual([]); }); // Bruch: bounds in PLACES passt nicht mehr zur Datei — der Datentest prüfte dann die falsche Fläche. diff --git a/tests/placesPipeline.test.js b/tests/placesPipeline.test.js index 6d78011..55e7fdd 100644 --- a/tests/placesPipeline.test.js +++ b/tests/placesPipeline.test.js @@ -3,7 +3,8 @@ 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, placeTarget } from "../tools/assets/places.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) { @@ -114,3 +115,25 @@ describe("placeTarget", () => { 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); + } + }); +}); diff --git a/tools/assets/places.mjs b/tools/assets/places.mjs index 1c8f008..7aa33cf 100644 --- a/tools/assets/places.mjs +++ b/tools/assets/places.mjs @@ -29,19 +29,24 @@ export const NEAR_MATERIALS = new Set([ "cont_a_side", "cont_a_end", "cont_b_side", "cont_b_end", "cont_c_side", "cont_c_end", "contFrame", "contHardware", "ironPlain", "ironTeal", "ironBlue", "osb", "osbB", "rollershutter_window_01", "rollershutter_window_01_graffiti", "chainlink_post", "chainlink_wire", + // Tribüne: die Quelle hat höchstens 768 px, compressTexture vergrößert nicht + "Sichtbeton", "Estrich", "Verkleidung", "Stahl", "Sitzschale", ]); /** - * Anteil der Dreiecke, der je Material bleibt; Container, Schuppen, Zaun und Boden fehlen hier und - * bleiben unverändert. Gemessen am Hood Yard nach flatten/join: die vier Köcherbäume haben zusammen - * 2,2 Mio. Dreiecke (quiver_tree_01_leaf 1,13 Mio., quiver_tree_02 739 000, quiver_tree_01_trunk - * 370 000), Gras je 60 000–150 000, die Mast-Kleinteile 92 000. + * Anteil der Dreiecke, der je Material bleibt; Gebäude, Boden und Zäune (Container, Schuppen, Tribüne mit + * Sitzen) fehlen hier und bleiben unverändert. Gemessen am Hood Yard nach flatten/join: die vier Köcherbäume + * haben zusammen 2,2 Mio. Dreiecke (quiver_tree_01_leaf 1,13 Mio., quiver_tree_02 739 000, + * quiver_tree_01_trunk 370 000), Gras je 60 000–150 000, die Mast-Kleinteile 92 000. An der Tribüne haben + * Kabel, Kabeldosen, Wandleuchten und Kästen zusammen 64 298. */ export const SIMPLIFY_RATIOS = new Map([ ["quiver_tree_01_leaf", 0.15], ["quiver_tree_01_trunk", 0.15], ["quiver_tree_02", 0.15], ["searsia_lucida", 0.5], ["searsia_lucida_leaves", 0.5], ["searsia_lucida_twigs", 0.5], ["grass_medium_01", 0.5], ["grass_medium_01_dry", 0.5], ["grass_medium_02", 0.5], ["modular_electricity_poles_pieces", 0.25], ["security_camera_01", 0.25], + ["modular_electric_cables", 0.25], ["modular_electric_cables_boxes", 0.25], ["industrial_wall_lamp", 0.25], + ["industrial_wall_lamp_glass", 0.25], ["power_box_01", 0.25], ["utility_box_01", 0.25], ]); // Anteil am Mesh-Radius. Nach join umspannt ein Pflanzenmaterial den ganzen Hof (~17 m Radius), die // Grenze liegt dann bei ~17 cm und greift kaum — die Anteile oben steuern (am Modell gemessen: 0,002