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:
1 parent
efe15c4b64
commit
5c8df78246
5 files changed
+143
-3
No files matched your search
+9
-1
@@ -22,7 +22,15 @@ renderer.toneMapping = ACESFilmicToneMapping;
|
||||
renderer.shadowMap.enabled = true;
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
const camera = new PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 2500);
|
||||
// Nahwert 0.1 -> 0.5 (Diagnose "Flackernder Boden"): ein 24-Bit-Tiefenpuffer löst bei 150-300 m
|
||||
// Kameraabstand (Downtown von oben, s. Diagnose-Report) nur 1,3-5 cm auf — in derselben
|
||||
// Größenordnung wie die 1-2 cm, die Boden/Stadtfläche/Park/Strand/Straße in geometry.js
|
||||
// auseinanderliegen. Fünffacher Nahwert = fünffache Tiefenauflösung auf jede Entfernung
|
||||
// (Auflösung skaliert mit dem Nahwert), macht die verbleibenden Abstände wieder auflösbar,
|
||||
// ohne selbst etwas an der Fern-/Sichtweite (far=2500) zu ändern. 0.5 m liegt deutlich unter
|
||||
// Kapsel-/Kameraradius und Wandstärken (s. Ersichtsprüfung im Bericht) — Ego-Perspektive an
|
||||
// einer Wand clippt damit nicht sichtbar.
|
||||
const camera = new PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.5, 2500);
|
||||
// Pflichtoption für player.init: der Controller übernimmt die Kamera, braucht aber controls.domElement als Eingabeziel.
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
scene.add(new AmbientLight(0xffffff, 1.5));
|
||||
|
||||
@@ -4,6 +4,14 @@ const GROUND_THICKNESS = 1.0;
|
||||
const SURFACE_OFFSET = 0.02; // Sand, Park und Stadtfläche liegen knapp über dem Boden
|
||||
const ROAD_OFFSET = 0.03; // Straßen wiederum knapp über den Flächen
|
||||
const BRIDGE_THICKNESS = 0.6;
|
||||
// Brückendeck lag bisher exakt bei y=0, identisch mit der Bodenoberkante (top weiter unten) —
|
||||
// garantiertes Z-Fighting überall dort, wo eine Brücke an Land endet, unabhängig von Kamera-
|
||||
// abstand oder Tiefenpuffer-Versatz (der hilft nur gegen knappe, aber von 0 verschiedene
|
||||
// Abstände). Dieselbe Größenordnung wie SURFACE_OFFSET: groß genug, um auch bei 300 m
|
||||
// Kameraabstand im Tiefenpuffer aufzulösen (s. materials.js-Kommentar zu POLYGON_OFFSETS),
|
||||
// klein genug, dass die Brücke weiterhin flach und ohne spürbare Stufe befahrbar bleibt — wie
|
||||
// die von SURFACE_OFFSET bereits unangetastet gebliebene Kollision auf den Bodenflächen.
|
||||
const BRIDGE_OFFSET = SURFACE_OFFSET;
|
||||
export const SURFACE_KINDS = ["sand", "park", "urban"];
|
||||
|
||||
/** Pastellton je Gebäude, reproduzierbar aus seiner Lage. */
|
||||
@@ -122,7 +130,7 @@ export function buildChunkGeometry(chunk) {
|
||||
const out = {
|
||||
ground: extrudePolygons(chunk.land, { base: -GROUND_THICKNESS, top: 0, uvScale: 8 }),
|
||||
roads: flatPolygons(roads, { y: ROAD_OFFSET, uvScale: 8 }),
|
||||
bridges: extrudePolygons(bridges, { base: -BRIDGE_THICKNESS, top: 0, uvScale: 8 }),
|
||||
bridges: extrudePolygons(bridges, { base: -BRIDGE_THICKNESS, top: BRIDGE_OFFSET, uvScale: 8 }),
|
||||
buildings: buildBuildings(chunk.buildings),
|
||||
};
|
||||
for (const kind of SURFACE_KINDS) {
|
||||
|
||||
@@ -72,6 +72,54 @@ async function loadSet(loader, slug, fallback, { tint, maxAnisotropy } = {}) {
|
||||
return material;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// Tiefenpuffer-Versatz gegen Flackern zwischen Boden, Stadtfläche, Park, Strand und Straße
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// Befund (Diagnose "Flackernder Boden"): 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.
|
||||
// Bei 150-300 m Kameraentfernung (Downtown von oben, wie im gemeldeten Screenshot) liegt das
|
||||
// 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 — s. main.js); Strand, Park und Stadtfläche liegen zusätzlich exakt
|
||||
// aufeinander (identischer y-Wert) und fighten deshalb IMMER, unabhängig von der Entfernung.
|
||||
//
|
||||
// polygonOffset verschiebt den Tiefenwert einer Fläche im Tiefenpuffer selbst, nicht in der
|
||||
// Geometrie — die Rangfolge gilt deshalb auf jede Entfernung, nicht nur dort, wo der
|
||||
// geometrische 1-2cm-Abstand noch auflösbar wäre. Reihenfolge vom Boden zur Straße, von der
|
||||
// allgemeinsten zur spezifischsten Fläche: eine Stadtfläche deckt die größte Fläche ab und
|
||||
// darf am ehesten unbemerkt unter einer Nachbarfläche verschwinden, eine Straße ist die
|
||||
// kleinteiligste und eindeutig erwartete Linie und muss immer sichtbar obenauf liegen. Strand,
|
||||
// Park und Stadtfläche bekommen dabei je eine eigene Stufe, statt sich (wie den geometrischen
|
||||
// SURFACE_OFFSET) eine zu teilen — sonst fighten sie weiterhin gegeneinander, nur auf einer
|
||||
// anderen Höhe.
|
||||
//
|
||||
// Jede Stufe: derselbe Betrag für Faktor und Units, streng steigend Richtung Kamera (negativ,
|
||||
// laut WebGL-Spezifikation zu glPolygonOffset: offset = factor*Steigung + units*r — r ist per
|
||||
// Spezifikation bereits "die kleinste von der Hardware garantiert auflösbare Tiefendifferenz",
|
||||
// ein Versatz von 1 Unit trennt zwei Flächen also auf jede Entfernung, ohne von einem
|
||||
// geometrischen Abstand abzuhängen, der bei 300 m längst unter der Auflösung verschwunden
|
||||
// wäre). factor deckt dabei schräge Sichtwinkel ab (von der Seite, hohe Tiefensteigung über die
|
||||
// Fläche), units den senkrechten Blick von oben (Steigung ~ 0) — der Spieler sieht im Spiel
|
||||
// beides, je nach Kamerawinkel (Vogelperspektive in fly mode wie im Befund, aber auch normale
|
||||
// Bodenperspektive).
|
||||
export const POLYGON_OFFSET_ORDER = ["ground", "urban", "park", "sand", "roads"];
|
||||
|
||||
export const POLYGON_OFFSETS = Object.fromEntries(
|
||||
POLYGON_OFFSET_ORDER.map((key, i) => [
|
||||
key,
|
||||
{ polygonOffset: true, polygonOffsetFactor: -i, polygonOffsetUnits: -i },
|
||||
]),
|
||||
);
|
||||
|
||||
/** Trägt POLYGON_OFFSETS in die passenden Materialien ein. Nur die fünf benannten Schichten —
|
||||
* Brücken (eigene, geometrische Lösung, s. geometry.js BRIDGE-Kommentar), Gebäude, Wasser und
|
||||
* Meeresboden sind eigene Materialobjekte ohne Coplanar-Risiko und bleiben unberührt. */
|
||||
export function applyPolygonOffsets(materials) {
|
||||
for (const [key, offset] of Object.entries(POLYGON_OFFSETS)) {
|
||||
if (materials[key]) Object.assign(materials[key], offset);
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
export async function createMaterials(maxAnisotropy) {
|
||||
const loader = new TextureLoader();
|
||||
const tints = { roads: ROAD_TINT, bridges: ROAD_TINT };
|
||||
@@ -81,6 +129,7 @@ export async function createMaterials(maxAnisotropy) {
|
||||
);
|
||||
const materials = Object.fromEntries(entries);
|
||||
materials.buildings.vertexColors = true; // pastellige Tönung je Gebäude
|
||||
applyPolygonOffsets(materials);
|
||||
|
||||
const normal = new DataTexture(waterNormalData(128), 128, 128, RGBAFormat);
|
||||
normal.wrapS = normal.wrapT = RepeatWrapping;
|
||||
|
||||
@@ -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
@@ -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({});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user