Files
gta6/docs/superpowers/specs/2026-09-14-hood-yard-design.md
T
knsoandClaude Opus 5 b61d9ab6a1 docs: Entwurf für den Hood Yard im Park J6/K6
Eigenes Modell aus three-d-stage 1:1 an die Stelle der kleinen Gebäude
zwischen Baseball- und Fußballfeld: Aufbereitung mit gltf-transform unter
Erhalt von EXT_materials_bump, Orte-Schicht mit Laden je nach Abstand,
Kollision über Materialnamen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 09:44:20 +02:00

14 KiB
Raw Blame History

Hood Yard im Park J6/K6 — Entwurf

Datum: 2026-09-14 Zusammenhang: Der Nutzer hat im eigenen Editor „three-d-stage“ einen Hof mit Containern, Schuppen, Zaun, Bäumen und Strommasten gebaut (~/Downloads/hood-yard.glb, dazu eine .mtl nur mit Materialnamen). Er soll 1:1 an die Stelle, an der im echten Flamingo Park (Miami Beach) die kleinen Gebäude zwischen Baseball- und Fußballfeld stehen. Das Spiel zeigt diesen Park in J6/K6, oben an der Grenze der beiden Planquadrate. Vorlage ist ein Satellitenbild des Nutzers (~/Desktop/Bildschirmfoto 2026-09-14 um 00.15.53.png, 1552 × 474 px). Baut auf: src/world/coords.js (Welt ↔ three, Planquadrate), src/world/props.js (GLTFLoader mit Meshopt-Decoder), src/world/streaming.js (Lade- und Entladeradius, Collider je Chunk), src/world/shadows.js (prepareLitMaterials), tools/assets/props.mjs (bisherige Modell-Aufbereitung über npx @gltf-transform/cli).

Ziel

Der Hof steht im Spiel an der richtigen Stelle, in Originalgröße, bündig auf dem Gelände, mit Schatten und Kollision, und das Spiel bleibt flüssig. Weitere Modelle aus dem Editor lassen sich später mit einem Listeneintrag und einem Skriptaufruf dazustellen.

Befund (gemessen)

Modell

Punkt Wert
Datei 653,6 MB, THREE.GLTFExporter r184, 1103 Knoten, 294 Meshes, 44 Materialien
Texturen 105 PNG in 2048 × 2048, davon 69 verschieden (409 MB)
Dreiecke 750 826, davon rund 560 000 in Bäumen und Büschen, 92 000 in Mast-Kleinteilen
Maße x −17,23 bis 17,00 · z −13,40 bis 8,60 · Höhe bis 8,94 m (Masten), also 34,2 × 22,0 m
Einheit Meter: ein Container ist 2,9 × 6,3 m und 2,6 m hoch
Ausrichtung Rückseite mit Zaun und Hintergrundbäumen bei −z, Front mit Straße und Toren bei +z. Im Spiel ist −z Norden, das passt ohne Drehung zum Satellitenbild.
Ursprung Mitte der Gebäudereihe: Container x −10,17 bis −0,72, Schuppen x −0,55 bis 10,35
Boden lot_gravel ist eine 34 cm dicke Kiesplatte von y 0 bis 0,34. Die Wurzel ist um 0,34 angehoben, alles andere steht auf der Plattenoberfläche.
Erweiterungen KHR_texture_transform, EXT_materials_bump, KHR_materials_emissive_strength
Wellblech Die sechs Container-Materialien (cont_*_side, cont_*_end) sind flache Kästen, ihr Relief kommt nur aus EXT_materials_bump. three r184 liest die Erweiterung, @gltf-transform 4.5.0 kennt sie nicht und verwirft sie beim Einlesen.

Stelle im Spiel (Chunk-Daten chunk_9_5, chunk_10_5)

  • Zwischen der Straße am Nordrand des Parks und dem Baseballfeld (surfaces, kind: "park") liegt ein Streifen Land: bei x 1472 von y 1057,25 bis 1082,2 (25 m), bei x 1506 von 1058,0 bis 1086,1 (28 m). Die Straße steigt nach Osten um etwa 5° an.
  • Das Fußballfeld beginnt bei x 1550,5, seine Oberkante liegt bei y 1073,5.
  • Im Bereich x 1465–1520, y 1055–1095 stehen keine Palmen und keine Gebäude. Zwei Laternen (vergeLampPoints, eine je Chunk) stehen bei (1478,1 | 1081,6) und (1478,0 | 1083,6).
  • Vom Startpunkt (x 1600, y 690) sind es 393 m bis zur Hofmitte.

Satellitenbild

  • Maßstab etwa 0,15 m je Pixel, geschätzt aus Autos und Feldmarkierungen (±15 %).
  • Anker: Nordost-Ecke des Baseballfelds, im Bild (1000 | 213), im Spiel (1525 | 1058).
  • Die echte Reihe ist etwa 37 m lang: drei Container bei x 1470,6–1487,9, danach eine Reihe kleiner Schuppen bis x 1507,8. Die Schuppenfronten liegen bei y ≈ 1066,7.
  • Die Reihe im Modell ist 20,5 m lang. Beide Enden können deshalb nicht zugleich passen.

Entscheidungen des Nutzers

Frage Entscheidung
Größe 1:1, nichts skalieren (Modell ist in Metern gebaut)
Lage Übergang von Containern zu Schuppen an derselben Stelle wie in echt: Hofmitte x 1489, y 1067,5, keine Drehung (Vergleichsbild im Gespräch bestätigt)
Weg ins Spiel Eigene Schicht „Orte“ mit Laden und Entfernen je nach Abstand, getrennt vom Chunk-Streaming
Aufbereitung Eigenes Node-Skript mit @gltf-transform, das EXT_materials_bump erhält
Abschnitte Aufbereitung, Laufzeit, Prüfung bestätigt
Arbeitsweise Eigener Arbeitsbaum, weil im Hauptordner nicht committete Änderungen einer anderen Sitzung liegen

Lage

at = [1489, 1067.5], rotation = 0, lift = −0.28.

Nachbar Abstand zur Hofkante (Außenmaße 34,2 × 22 m: x 1471,8–1506,0, y 1058,9–1080,9)
Straßenrand im Norden 1,3 m (Westende) bis 5,2 m (Ostende)
Baseballfeld im Süden 1,65 m (Westende) bis 0,9 m (Ostende)
Laterne (1478,1 | 1081,6) 0,7 m nördlich der Kante, die zweite Laterne 2,7 m
Fußballfeld 44 m östlich
J6/K6-Grenze (x 1500) 0,65 m östlich des letzten Schuppens

Höhe: Mit lift = −0.28 liegt die Kiesoberfläche 6 cm über dem Gelände (Oberkante Land y = 0). Man läuft ohne Stufe hinein. Bei 600 m Abstand löst der Tiefenpuffer etwa 4,3 cm auf (Nahwert 0,5 m, 24 Bit), 6 cm flackern also nicht. Alle Teile sinken mit, die Maße im Modell bleiben gleich.

Aufbau

Einheit Aufgabe
tools/assets/places.mjs Neu. node tools/assets/places.mjs <quelle.glb> <id> schreibt public/models/places/<id>.glb und gibt einen Vorher-Nachher-Bericht aus.
tools/assets/bumpExtension.mjs Neu. Durchreich-Erweiterung EXT_materials_bump für @gltf-transform: bumpFactor und bumpTexture als echte Texturreferenz, damit Dedup, Prune und Texturkompression sie sehen.
src/world/places.js Neu. PLACES (Liste), reine Helfer placeTransform(place), wantedPlaces(position, places, { load, unload, active }), isSolidMaterial(name) und die Klasse PlaceLayer.
src/main.js Legt PlaceLayer neben dem ChunkStreamer an und ruft in tick() direkt nach streamer.update places.update(playerPosition(player)).
package.json Neue Entwicklungsabhängigkeiten @gltf-transform/core, @gltf-transform/extensions, @gltf-transform/functions (4.5.0), sharp, meshoptimizer.
public/models/places/hood-yard.glb Neu, versioniert. Die Quelldatei bleibt außerhalb des Repos.

Aufbereitung (tools/assets/places.mjs)

Reihenfolge:

  1. Einlesen mit NodeIO, allen Erweiterungen aus @gltf-transform/extensions und bumpExtension.mjs.
  2. dedup() und prune(): doppelte Texturen, Materialien, Meshes und Accessoren zusammenlegen, Ungenutztes entfernen (105 → 69 Texturen).
  3. flatten() und join(): alle Mesh-Knoten direkt unter die Szene, danach Primitive je Material zu einem zusammenfügen. Aus rund 300 Draw Calls werden etwa 45. Materialnamen bleiben erhalten.
  4. weld() und simplifyPrimitive() mit MeshoptSimplifier, nur für Primitive dieser Materialien: quiver_tree_01_leaf, quiver_tree_01_trunk, quiver_tree_02, searsia_lucida, searsia_lucida_leaves, searsia_lucida_twigs, grass_medium_01, grass_medium_01_dry, grass_medium_02, modular_electricity_poles_pieces, security_camera_01. Die Stärke (ratio, error) wird am Bild im Spiel eingestellt und im Skript als Konstante festgehalten.
  5. textureCompress() mit sharp nach WebP.
    • 1024 px für Texturen der Nahflächen: gravel, asphalt, curbAsphalt, padAsphalt, puddleAsphalt, cobble, grass, cont_*, contFrame, contHardware, ironPlain, ironTeal, ironBlue, osb, osbB, rollershutter_window_01, rollershutter_window_01_graffiti, chainlink_post, chainlink_wire.
    • 512 px für alle übrigen.
    • Hängt eine Textur nach dem Dedup an Materialien beider Gruppen, gilt die größere Kante.
    • Normal-Maps werden mit höherer Qualität kodiert als Farb- und Rauheitskarten, damit keine Blockmuster im Licht entstehen.
  6. meshopt() mit MeshoptEncoder: Quantisierung und EXT_meshopt_compression, wie bei der Straßenlaterne.

Bewusst nicht: instance() (die Kollision des Controllers liest nur mesh.geometry mit matrixWorld, Instanzmatrizen gingen verloren), palette() (fasst texturlose Materialien unter neuem Namen zusammen), KTX2 (bräuchte toktx und einen Transcoder im Spiel).

Bericht: Megabyte, Texturen (Anzahl, Kantenlängen), Dreiecke, Primitive, geschätzter Grafikspeicher (RGBA mit Mipmaps), Außenmaße. Ziel: unter 30 MB.

Orte-Schicht (src/world/places.js)

Liste

export const PLACES = [
    { id: "hood-yard", url: "./models/places/hood-yard.glb", at: [1489, 1067.5], rotation: 0, lift: -0.28 },
];
  • at: Welt-x und Welt-y in Metern, wie auf der Minimap. Nie three-z.
  • rotation: Grad gegen den Uhrzeigersinn, von oben mit Norden oben gesehen. Das ist dieselbe Festlegung wie bei add_building in corrections.toml (docs/superpowers/specs/2026-09-13-feinkalibrierung-design.md).
  • lift: Meter, auf die Modellwurzel.

Umrechnung

placeTransform(place) liefert { position: { x: at[0], y: lift, z: -at[1] }, rotationY: rotation · π / 180 }. Positives rotationY dreht in three +x nach −z, von oben also gegen den Uhrzeigersinn.

Laden und Entfernen

  • wantedPlaces misst vom Punkt at: Laden bei höchstens 400 m, Entfernen erst über 600 m. Das sind dieselben Radien wie ChunkStreamer.update.
  • PlaceLayer.update(position) stößt das Laden an (ein zwischengespeichertes Promise je Ort, GLTFLoader mit MeshoptDecoder) und setzt einen fertig geladenen Ort ein, sobald er gewünscht ist. Ist der Spieler beim Ende des Ladens schon weiter als 600 m entfernt, bleibt der Ort draußen, bis er wieder näher als 400 m kommt.
  • Einsetzen: Wurzel an placeTransform, jedes Mesh castShadow und receiveShadow, nicht feste Meshes userData.excludeFromCollider = true, scene.add, danach player.addCollider({ motion: "static", useWorker: true, shape: { kind: "mesh", source: wurzel } }).
  • Entfernen: scene.remove und player.removeCollider(handle). Die geladenen Daten bleiben erhalten, eine Rückkehr setzt sofort wieder ein. Kosten: rund 200 MB Grafikspeicher, sobald der Hof einmal geladen war (der Bericht der Aufbereitung nennt den genauen Wert).
  • Die Kaskaden-Schatten richten die neuen Materialien selbst ein (prepareLitMaterials in sun.update()).

Kollision

isSolidMaterial(name) gibt für diese Materialien true zurück, alle anderen laufen nicht in den Collider:

Gruppe Materialien
Boden gravel, asphalt, curbAsphalt, padAsphalt, puddleAsphalt, cobble, grass, concrete
Container cont_a_side, cont_a_end, cont_b_side, cont_b_end, cont_c_side, cont_c_end, contFrame
Schuppen und Tore ironPlain, ironTeal, ironBlue, galv, rust, darkTrim, osb, osbB, rollershutter_window_01, rollershutter_window_01_graffiti
Zaun chainlink_post, chainlink_wire
Masten, Fass, Kiste modular_electricity_poles, barrel_03, plastic_container
Bäume quiver_tree_01_trunk, quiver_tree_02

Durchlässig bleiben Gras (grass_medium_*), Blätter und Zweige, die Büsche (searsia_lucida*), contHardware, modular_electricity_poles_pieces, security_camera_01* und trashbag.

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.
  • 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.

Tests

  • tests/places.test.js:
    • placeTransform: Welt-y wird zu −z, lift landet in y, 90° drehen +x nach −z.
    • wantedPlaces: lädt bei 390 m, entfernt bei 500 m nicht, entfernt bei 650 m, meldet nichts doppelt an.
    • isSolidMaterial: je ein Material jeder Gruppe fest, grass_medium_01 und quiver_tree_01_leaf nicht.
    • PlaceLayer mit Attrappen für Szene, Spieler (addCollider, removeCollider) und Loader: Einsetzen meldet genau einen Collider an und markiert nur nicht feste Meshes; Entfernen meldet ihn ab; Rückkehr setzt ohne neues Laden ein; ein Ladefehler gibt eine Warnung und wirft nicht.
  • Datentest gegen public/data/vice-a/chunk_9_5.json und chunk_10_5.json: Das Rechteck der Außenmaße an seiner Lage schneidet kein Straßen- oder Gebäudepolygon, liegt ganz auf land und enthält keine Palme und keinen Punkt aus vergeLampPoints(lampPoints(...)).
  • Asset-Test auf public/models/places/hood-yard.glb (liest nur den JSON-Teil der Datei): kleiner als 30 MB; extensionsUsed enthält EXT_meshopt_compression, EXT_texture_webp und EXT_materials_bump, aber nicht EXT_mesh_gpu_instancing; alle Bilder image/webp; jedes Material aus isSolidMaterial existiert.
  • Browser:
    • Über die Minimap zum Hof springen.
    • Screenshots in Augenhöhe von der Straße und vom Baseballfeld aus, dazu eine Draufsicht zum Vergleich mit dem Satellitenbild.
    • Gegen Container und Zaun laufen, durch Gras laufen.
    • Konsole ohne Fehler. renderer.info (Draw Calls, Texturen, Geometrien) und Ladezeit festhalten.

Nicht Teil dieser Arbeit

  • Die Minimap zeigt den Hof nicht, ihr Kartenbild kommt aus der Extraktion.
  • Keine Detailstufen und kein Ausblenden von Palmen oder Laternen. An dieser Stelle überschneidet sich nichts, der Datentest wacht darüber.
  • Das Modell des Nutzers wird nur verkleinert, nicht umgebaut.
  • Kein Sprungziel im Debug-Panel. Die Minimap springt schon an jede Stelle.
  • Die Feinkalibrierung (corrections.toml) bleibt eine eigene Aufgabe.