From 7e4af1d0bcf40b2cfa8a2ad3b31c4352548c9ad9 Mon Sep 17 00:00:00 2001 From: KNSO Date: Mon, 14 Sep 2026 13:43:23 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Hood-Yard-Spec=20mit=20Messwerten=20und?= =?UTF-8?q?=20Vorw=C3=A4rmen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wortlaut: die Radien garantieren nur, dass die Chunks unter einem Ort angefordert sind, solange er in der Szene ist — nicht, dass ihr Boden schon gebaut ist (ChunkStreamer baut höchstens einen Chunk je Bild). "geladen" -> "angefordert" im Kommentar über den Radien (src/world/places.js), über der PlaceLayer-Konstruktion (src/main.js), im Testnamen (tests/placesSite.test.js) und in der Spec, dort mit einem Satz zu den paar Bildern nach einem Kartensprung. Spec zusätzlich: warmUp in den Bullets "Vorbereiten"/"Einsetzen", robustes Einsetzen (Warnung, kein erneuter Versuch) im Bullet zum Ladefehler, und ein neuer Messwerte-Bullet in "Fehlerfälle und Risiken" (+0,4–0,6 ms je Bild, ~130 ms einmaliger Texturupload ohne warmUp). Co-Authored-By: Claude Sonnet 5 --- docs/superpowers/specs/2026-09-14-hood-yard-design.md | 9 +++++---- src/main.js | 2 +- src/world/places.js | 2 +- tests/placesSite.test.js | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/superpowers/specs/2026-09-14-hood-yard-design.md b/docs/superpowers/specs/2026-09-14-hood-yard-design.md index 30e3d71..2d8ef5f 100644 --- a/docs/superpowers/specs/2026-09-14-hood-yard-design.md +++ b/docs/superpowers/specs/2026-09-14-hood-yard-design.md @@ -143,11 +143,11 @@ export const PLACES = [ | `PLACE_SHOW_RADIUS` | 250 m | einsetzen | | `PLACE_HIDE_RADIUS` | 450 m | herausnehmen | - Chunks laden bei 400 m und bauen bei 600 m ab, beides gemessen zu ihrem Mittelpunkt. Der fernste Chunk-Mittelpunkt unter dem Hof liegt 148 m von dessen Mitte entfernt. 250 + 148 ≤ 400 und 450 + 148 ≤ 600 heißt: Solange der Hof in der Szene ist, ist sein Boden geladen. Der Datentest prüft das für jeden Ort. + Chunks laden bei 400 m und bauen bei 600 m ab, beides gemessen zu ihrem Mittelpunkt. Der fernste Chunk-Mittelpunkt unter dem Hof liegt 148 m von dessen Mitte entfernt. 250 + 148 ≤ 400 und 450 + 148 ≤ 600 heißt: Solange der Hof in der Szene ist, ist sein Boden angefordert. `ChunkStreamer` baut aber höchstens einen Chunk je Bild; direkt nach einem Kartensprung neben den Hof kann er deshalb ein paar Bilder lang vor seinem Boden erscheinen. Der Datentest prüft das für jeden Ort. - `wantedPlaces(position, places, { show, hide, active })` arbeitet wie `wantedChunks`: `add` ab `show`, `remove` erst jenseits von `hide`. - `PlaceLayer.update(position)` stößt innerhalb von 600 m das Laden an. Dafür gibt es je Ort ein einziges Laden mit `GLTFLoader` und `MeshoptDecoder`. Danach setzt es einen fertig geladenen Ort ein oder nimmt ihn heraus. Ist der Spieler beim Ende des Ladens schon jenseits von 450 m, bleibt der Ort draußen, bis er wieder näher als 250 m kommt. -- **Vorbereiten** (`prepareModel`, einmal nach dem Laden): Wurzel an `placeTransform`, jedes Mesh `castShadow` und `receiveShadow`, nicht feste Meshes `userData.excludeFromCollider = true`. Hat kein Mesh ein festes Material, gibt es eine Warnung und später keinen Collider (eine leere Quelle liefert im Controller nur ein Handle, das nie fertig wird, vgl. Ruling 51). -- **Einsetzen:** `scene.add`, danach `player.addCollider({ motion: "static", useWorker: true, shape: { kind: "mesh", source: wurzel } })`. +- **Vorbereiten** (`prepareModel`, einmal nach dem Laden): Wurzel an `placeTransform`, jedes Mesh `castShadow` und `receiveShadow`, nicht feste Meshes `userData.excludeFromCollider = true`. Hat kein Mesh ein festes Material, gibt es eine Warnung und später keinen Collider (eine leere Quelle liefert im Controller nur ein Handle, das nie fertig wird, vgl. Ruling 51). Ist ein `warmUp` gesetzt, läuft es danach einmalig (siehe „Gemessen im Spiel" unten). +- **Einsetzen:** Collider-Handle bauen (`player.addCollider({ motion: "static", useWorker: true, shape: { kind: "mesh", source: wurzel } })`), erst danach `scene.add` — wirft `addCollider`, bleibt die Szene unverändert (siehe Fehlerfälle). - **Entfernen:** `scene.remove` und `player.removeCollider(handle)`. Die geladenen Daten bleiben erhalten, eine Rückkehr setzt sofort wieder ein. Kosten: geschätzt 231 MB Grafikspeicher, sobald der Hof einmal gezeichnet war. - Die Kaskaden-Schatten richten die neuen Materialien selbst ein (`prepareLitMaterials` in `sun.update()`). @@ -170,12 +170,13 @@ Positionen fester Meshes, die quantisiert vorliegen (KHR_mesh_quantization aus ` ## Fehlerfälle und Risiken -- **Datei lädt nicht:** `console.warn` mit Ort und Fehler, der Ort gilt als fehlgeschlagen und wird nicht erneut versucht. Das Spiel läuft ohne ihn weiter. +- **Datei lädt nicht oder lässt sich nicht einsetzen:** `console.warn` mit Ort und Fehler, der Ort gilt als fehlgeschlagen und wird nicht erneut versucht — auch wenn `addCollider` beim Einsetzen wirft (die Szene bleibt dann unverändert statt einer Wurzel ohne Collider). Das Spiel läuft ohne ihn weiter. - **Entfernen, bevor der Collider fertig ist:** `removeCollider` räumt auch unfertige Worker-Collider ab, genau wie beim Chunk-Abbau. - **Beleuchtung weicht vom Editor ab:** Das Spiel leuchtet mit HDRI ×5 und Sonne 2,5. Wirkt der Hof zu hell oder zu dunkel, wird das im Browser verglichen und im Gespräch entschieden, nicht still angepasst. - **Vereinfachung dünnt Blätter aus:** Die Stärke wird nur an Pflanzen und Kleinteilen eingestellt, mit Vorher-Nachher-Screenshot. - **Collider zu schwer:** Wird der BVH-Aufbau im Worker spürbar langsam, fallen die Baumkronen (`quiver_tree_02`) aus der Kollisionsliste. - **Neue Karte:** Erzeugt die Pipeline Straßen, Palmen oder Laternen neu, prüft der Datentest die Hoffläche erneut. +- **Gemessen im Spiel (2026-09-14, 2560 × 1600, mit Schattenstufen):** +0,4–0,6 ms je Bild (Blick vom Baseballfeld 2,7–2,9 ms statt 2,3 ms, im Hof 2,1 statt 1,6 ms). Das erste Zeichnen kostete einmalig rund 130 ms Texturupload; `warmUp` legt das und die Shader-Übersetzung ins Vorladen. ## Tests diff --git a/src/main.js b/src/main.js index 62bd894..1db8b64 100644 --- a/src/main.js +++ b/src/main.js @@ -257,7 +257,7 @@ async function warmUpPlace(root) { await renderer.compileAsync(root, camera, scene); } // Fest platzierte Modelle aus three-d-stage (src/world/places.js). Eigene Radien, damit ein Ort nur in -// der Szene ist, solange die Chunks unter ihm geladen sind. +// der Szene ist, solange die Chunks unter ihm angefordert sind. const places = new PlaceLayer({ scene, player, warmUp: warmUpPlace }); player.setSkipCapsuleCollision(true); let settled = false; diff --git a/src/world/places.js b/src/world/places.js index 4273b2a..2fdf163 100644 --- a/src/world/places.js +++ b/src/world/places.js @@ -9,7 +9,7 @@ import { worldToThree } from "./coords.js"; // Chunks laden bei 400 m und bauen bei 600 m ab, gemessen zu ihrem Mittelpunkt (ChunkStreamer.update). // Der fernste Chunk-Mittelpunkt unter dem Hood Yard liegt 148 m von dessen Mitte entfernt: erschiene // der Hof schon bei 400 m, stünde er vom Startpunkt aus über dem Meer. Mit 250 m und 450 m ist sein -// Boden geladen, solange er in der Szene ist (tests/placesSite.test.js prüft das für jeden Ort). +// Boden angefordert, solange er in der Szene ist (tests/placesSite.test.js prüft das für jeden Ort). // Heruntergeladen wird früher, damit das Einsetzen nicht auf die Datei wartet. export const PLACE_PRELOAD_RADIUS = 600; export const PLACE_SHOW_RADIUS = 250; diff --git a/tests/placesSite.test.js b/tests/placesSite.test.js index b45d5e3..3ab60ae 100644 --- a/tests/placesSite.test.js +++ b/tests/placesSite.test.js @@ -86,9 +86,9 @@ describe.each(PLACES.map((place) => [place.id, place]))("Ort %s auf der Karte", expect(props.filter(({ X, Y }) => insideBounds(place, X, Y))).toEqual([]); }); - // Bruch: der Ort erscheint, bevor sein Boden geladen ist, und steht dann über dem Meer — oder der - // Boden wird abgebaut, während der Ort noch in der Szene ist. - it("ist nur in der Szene, solange die Chunks unter ihm geladen sind", () => { + // Bruch: der Ort erscheint, bevor der Boden unter ihm angefordert ist, und steht dann über dem + // Meer — oder der Boden wird abgebaut, während der Ort noch in der Szene ist. + it("ist nur in der Szene, solange die Chunks unter ihm angefordert sind", () => { const { position } = placeTransform(place); const farthest = Math.max(...under.map(([i, j]) => Math.hypot( SLICE.x0 + (i + 0.5) * CHUNK_SIZE - position.x,