fix(world): Flackernden Boden behoben (Tiefenpuffer-Versatz je Schicht, Brücke angehoben)

Downtown aus der Luft: Boden, Stadtfläche, Park, Strand und Straße lagen 1-2 cm
auseinander (geometry.js: SURFACE_OFFSET=0.02 für alle drei Flächen gleich,
ROAD_OFFSET=0.03), bei 150-300 m Kameraabstand unter der Auflösung eines
24-Bit-Tiefenpuffers (~1,3 cm bei 150 m, ~5 cm bei 300 m, gemessen mit dem alten
Nahwert 0.1) — Strand/Park/Stadtfläche lagen zusätzlich exakt aufeinander und
fighteten unabhängig von der Entfernung. Die Brückenoberkante lag exakt auf der
Bodenoberkante (beide y=0).

Vier Änderungen, freigegebener Entwurf:
- Kamera-Nahwert 0.1 -> 0.5 m (src/main.js): fünffache Tiefenauflösung auf jede
  Entfernung, far bleibt 2500.
- src/world/materials.js: POLYGON_OFFSETS/POLYGON_OFFSET_ORDER, eine feste,
  streng zunehmend negative Stufe je Schicht (Boden 0, Stadt -1, Park -2, Strand -3,
  Straße -4, für Faktor und Units gleich) — wirkt im Tiefenpuffer selbst, hält die
  Rangfolge deshalb auf jede Entfernung, unabhängig vom geometrischen Abstand.
  applyPolygonOffsets() ist eine separate, reine Funktion, damit sie ohne
  createMaterials()/Texturladen testbar ist und beweisbar nur diese fünf Materialien
  trifft (Brücken/Gebäude/Wasser/Meeresboden bleiben unberührt).
- Strand, Park und Stadtfläche bekommen dabei je eine eigene Stufe, statt sich wie
  bisher (SURFACE_OFFSET) eine zu teilen.
- src/world/geometry.js: Brückendeck-Oberkante jetzt BRIDGE_OFFSET (=SURFACE_OFFSET,
  0.02 m) statt 0 — derselbe Größenordnungsentscheid wie bei den Flächen, klein genug
  für einen unbemerkten Schritt, groß genug für die Tiefenpuffer-Auflösung bei 300 m.

TDD: Rangfolge/Kein-Teilen-Regeln sind rein und zuerst als fehlschlagende Tests
geschrieben (tests/materials.test.js, 5 neue), ebenso die Brückenanhebung
(tests/geometry.test.js, 1 neu) — beide zunächst rot, dann grün. 65 -> 71 Tests,
alle grün.

Browser-Verifikation (Downtown, Chunk 2_9, gleiche Kameraposition vorher/nachher):
das zuvor durchgängig zackige/zerstückelte Straßen-/Flächenbild ist jetzt glatt und
durchgängig lesbar. Ego-Perspektive dicht an einer Gebäudewand zeigt mit near=0.5
kein sichtbares Durchsehen. Details, Screenshot-Pfade und offene Punkte (Brücken-
Nahaufnahme nicht separat bestätigt) im Bericht:
.superpowers/sdd/2026-09-12-leonida-vice-slice/schichten-report.md

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-13 09:01:36 +02:00
1 parent efe15c4b64
commit 5c8df78246
5 files changed
+143 -3

No files matched your search

+22
View File
@@ -63,6 +63,28 @@ describe("geometry", () => {
expect(box.max.y).toBeCloseTo(0.03);
});
// Flackernder Boden (Diagnose-Report, Punkt 4): Brückendeck und Boden lagen beide exakt bei
// y=0 — identische Höhe, garantiertes Z-Fighting an jeder Stelle, an der eine Brücke an Land
// endet, unabhängig von Kameraabstand oder Tiefenpuffer-Versatz. Die Brückenoberseite muss
// jetzt oberhalb von y=0 liegen, ohne eine für den Spieler wahrnehmbare Stufe zu erzeugen.
it("hebt die Brückenoberseite über den Boden (kein gemeinsames y=0 mehr)", () => {
const chunk = {
chunk: [0, 0], origin: [0, 0],
land: [PLOT], surfaces: [],
roads: [{ poly: PLOT, bridge: true }],
centerlines: [], palms: [], buildings: [],
};
const out = buildChunkGeometry(chunk);
const groundTop = new Box3().setFromBufferAttribute(out.ground.getAttribute("position")).max.y;
const bridgeTop = new Box3().setFromBufferAttribute(out.bridges.getAttribute("position")).max.y;
expect(groundTop).toBeCloseTo(0);
expect(bridgeTop).toBeGreaterThan(groundTop);
// Bewusst klein (Größenordnung der bestehenden SURFACE_OFFSET/ROAD_OFFSET-Abstände,
// s. Kommentar in geometry.js): groß genug, um im Tiefenpuffer aufzulösen, klein genug,
// um beim Gehen/Fahren nicht als Stufe aufzufallen — "flach und begehbar wie bisher".
expect(bridgeTop - groundTop).toBeLessThan(0.1);
});
it("baut alle Teile eines Chunks", () => {
const chunk = {
chunk: [0, 0], origin: [0, 0],
+54 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { MATERIAL_KEYS, TEXTURES, waterNormalData } from "../src/world/materials.js";
import { applyPolygonOffsets, MATERIAL_KEYS, POLYGON_OFFSETS, POLYGON_OFFSET_ORDER, TEXTURES,
waterNormalData } from "../src/world/materials.js";
import { buildChunkGeometry } from "../src/world/geometry.js";
describe("materials", () => {
@@ -31,3 +32,55 @@ describe("materials", () => {
expect(blue.reduce((a, b) => a + b, 0) / blue.length).toBeGreaterThan(200);
});
});
// Flackernder Boden (Diagnose-Report): geometry.js legt Boden bei y=0, Strand/Park/Stadtfläche
// bei SURFACE_OFFSET=0.02 und Straßen bei ROAD_OFFSET=0.03 an — 1-2 cm Abstand, der bei
// 150-300 m Kameraentfernung unter der Auflösung eines 24-Bit-Tiefenpuffers liegt. Rein und
// ganz ohne Browser testbar (wie im Auftrag gefordert): POLYGON_OFFSETS ist eine reine
// Datenstruktur, applyPolygonOffsets() eine reine Funktion auf Platzhalterobjekten — beide
// brauchen weder createMaterials() noch echtes Texturladen.
describe("Tiefenpuffer-Versatz je Schicht (gegen Flackern zwischen Boden/Stadt/Park/Strand/Straße)", () => {
it("ordnet die Schichten Boden < Stadtfläche < Park < Strand < Straße", () => {
expect(POLYGON_OFFSET_ORDER).toEqual(["ground", "urban", "park", "sand", "roads"]);
});
it("aktiviert polygonOffset für jede der fünf Schichten", () => {
for (const key of POLYGON_OFFSET_ORDER) {
expect(POLYGON_OFFSETS[key]?.polygonOffset).toBe(true);
}
});
it("kein Paar unter den fünf Schichten teilt sich Faktor und Units — Strand, Park und "
+ "Stadtfläche bekommen je eine eigene Stufe statt sich eine zu teilen", () => {
const sigs = POLYGON_OFFSET_ORDER.map((key) => {
const o = POLYGON_OFFSETS[key];
return `${o.polygonOffsetFactor}/${o.polygonOffsetUnits}`;
});
expect(new Set(sigs).size).toBe(sigs.length);
});
it("Betrag von Faktor und Units steigt streng von Boden zu Straße (Richtung Kamera)", () => {
const factors = POLYGON_OFFSET_ORDER.map((key) => POLYGON_OFFSETS[key].polygonOffsetFactor);
const units = POLYGON_OFFSET_ORDER.map((key) => POLYGON_OFFSETS[key].polygonOffsetUnits);
for (let i = 1; i < factors.length; i++) {
// Zunehmend negativ = zunehmend Richtung Kamera gezogen, s. Kommentar in materials.js.
expect(factors[i]).toBeLessThan(factors[i - 1]);
expect(units[i]).toBeLessThan(units[i - 1]);
}
});
it("trägt den Versatz nur in Boden/Stadtfläche/Park/Strand/Straße ein — Brücken, Gebäude, "
+ "Wasser und Meeresboden bleiben unberührt (eigene Materialobjekte, s. Auftrag)", () => {
const fake = Object.fromEntries(
["ground", "urban", "park", "sand", "roads", "bridges", "buildings", "water", "seafloor"]
.map((k) => [k, {}]),
);
applyPolygonOffsets(fake);
for (const key of POLYGON_OFFSET_ORDER) {
expect(fake[key]).toMatchObject(POLYGON_OFFSETS[key]);
}
for (const key of ["bridges", "buildings", "water", "seafloor"]) {
expect(fake[key]).toEqual({});
}
});
});