Ablauf wie beim Hood Yard mit den gefundenen Fallstricken (Dreiecke je Knoten, Bodenplatte +6 cm, globale Materiallisten, Asset-Test je Ort, launch.json nur im Hauptordner, gl.finish-Messung, Vorwärmen prüfen) und Werkzeugen: Modell vermessen, Kartendaten eines Weltfensters, Vergleichsbild Satellit/Spiel, Screenshot-Dekodierung, erprobte Browser-Ausschnitte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
7.8 KiB
Markdown
124 lines
7.8 KiB
Markdown
# Browserprüfung eines Orts (verborgener Browser-Bereich)
|
||
|
||
Alle Ausschnitte sind am Hood Yard erprobt (2026-09-14). `javascript_tool` bricht nach 45 s ab: lange Abläufe auf mehrere Aufrufe verteilen. Jeder Aufruf hat eigene Variablen — was später gebraucht wird, an `window` hängen.
|
||
|
||
## 1. Dev-Server
|
||
|
||
- Das Vorschau-Werkzeug liest Server nur aus `.claude/launch.json` des **Hauptordners**. Auf 5173 läuft oft schon der Hauptordner.
|
||
- Arbeitet die Sitzung in einem Arbeitsbaum, darf sie diese Datei nicht ändern: Nutzer fragen, dann `ExitWorktree(keep)` → Eintrag setzen → `preview_start` → `EnterWorktree(path)`. Am Ende wieder raus und den Eintrag entfernen.
|
||
|
||
```json
|
||
{ "name": "place-check", "runtimeExecutable": "bash",
|
||
"runtimeArgs": ["-lc", "cd /Users/Kenso/Developer/gta6/.claude/worktrees/<name> && npx vite --port 5181 --strictPort"],
|
||
"port": 5181, "url": "http://localhost:5181" }
|
||
```
|
||
|
||
- Arbeitsbaum vorbereiten: `cp -Rc node_modules <baum>/node_modules`, dazu `three-player-controller/package.json` und `dist/` in `<baum>/three-player-controller/` kopieren. Git ignoriert Dateien im nicht ausgecheckten Submodul.
|
||
|
||
## 2. Debug-Seite (nie committen, danach löschen)
|
||
|
||
Der verborgene Bereich meldet oft 0 × 0 px, `requestAnimationFrame` steht, und three's `Timer` liefert delta 0, solange `document.hidden` gilt. Vor jedem Zug `resize_window` auf 1280 × 800 setzen (die App setzt es am Zugende zurück). `index-debug.html` im Root: Kopie von `index.html` mit diesem Skript im `<head>` vor dem Modul-Skript:
|
||
|
||
```html
|
||
<script>
|
||
(() => {
|
||
Object.defineProperty(document, "hidden", { configurable: true, get: () => false });
|
||
Object.defineProperty(document, "visibilityState", { configurable: true, get: () => "visible" });
|
||
const queue = new Map(); let nextId = 1;
|
||
window.requestAnimationFrame = (cb) => { const id = nextId++; queue.set(id, cb); return id; };
|
||
window.cancelAnimationFrame = (id) => { queue.delete(id); };
|
||
const worker = new Worker(URL.createObjectURL(new Blob(["setInterval(() => postMessage(0), 33);"], { type: "text/javascript" })));
|
||
worker.onmessage = () => { const cbs = [...queue.values()]; queue.clear(); const now = performance.now(); for (const cb of cbs) cb(now); };
|
||
})();
|
||
</script>
|
||
```
|
||
|
||
## 3. Szene, Kamera, Renderer greifbar machen
|
||
|
||
Wirft ein Hook, stirbt die Zeichenschleife endgültig. Vite lädt die Seite bei Abhängigkeits-Rebuilds neu (`performance.now()` klein), dann ist `window.__probe` weg — deshalb `??=`.
|
||
|
||
```js
|
||
const deps = performance.getEntriesByType("resource").map((e) => e.name);
|
||
const THREE = await import(deps.find((n) => n.includes("/node_modules/.vite/deps/three.js")));
|
||
window.__frames = [];
|
||
THREE.Scene.prototype.onBeforeRender = function (renderer, scene, camera) {
|
||
Object.assign((window.__probe ??= {}), { renderer, scene, camera });
|
||
window.__frames.push(performance.now());
|
||
if (window.__frames.length > 4000) window.__frames.splice(0, 2000);
|
||
if (window.__applyView) window.__applyView(camera);
|
||
};
|
||
```
|
||
|
||
## 4. Hinspringen über die Minimap
|
||
|
||
`M` per `keydown`/`keyup` (`code: "KeyM"`) auf `window`. Das Kartenbild bleibt im verborgenen Bereich bei „Karte lädt …“, bis ein **Screenshot** gemacht wurde. Danach klicken, mit `X`, `Y` = Welt-Ziel neben dem Ort (nicht auf ein Gebäude):
|
||
|
||
```js
|
||
const map = [...document.querySelectorAll("canvas")].find((c) => c !== window.__probe.renderer.domElement);
|
||
const rect = map.getBoundingClientRect(), dpr = map.width / rect.width;
|
||
const scale = Math.min(map.width / 3000, map.height / 3000);
|
||
const ox = (map.width - 3000 * scale) / 2, oy = (map.height - 3000 * scale) / 2;
|
||
const opts = { clientX: rect.left + ((X + 1000) * scale + ox) / dpr, clientY: rect.top + ((2500 - Y) * scale + oy) / dpr, button: 0, bubbles: true };
|
||
map.dispatchEvent(new MouseEvent("mousedown", opts)); map.dispatchEvent(new MouseEvent("mouseup", opts));
|
||
```
|
||
|
||
Die Wurzel des Orts findet man über ihre Lage: `scene.children.find((o) => Math.abs(o.position.x - at[0]) < 0.01 && Math.abs(o.position.z + at[1]) < 0.01)`.
|
||
|
||
## 5. Zustand prüfen
|
||
|
||
- **Kollisionsmarken:** Mit `root.traverse` die Meshes ohne `userData.excludeFromCollider` sammeln. Ihre Materialnamen müssen genau die festen Teile sein.
|
||
- **Quantisierung:** Alle festen Meshes haben `geometry.attributes.position.array instanceof Float32Array`, dafür sorgt `prepareModel`. Fehlt das, landet der Collider bei ±1 m um den Weltursprung.
|
||
- **Vorwärmen:** Vor dem Sprung ist der Ort noch nicht in der Szene. `renderer.info.memory.textures` liegt am Startpunkt trotzdem schon um die Texturzahl des Orts höher.
|
||
|
||
## 6. Blicke aufnehmen
|
||
|
||
Kamera übersteuern, auch in einem Wrapper um `CSM.prototype.update`, sonst fehlen die Schatten im Bild:
|
||
|
||
```js
|
||
const { CSM } = await import(deps.find((n) => n.includes("three_examples_jsm_csm_CSM")));
|
||
const eye = new THREE.Vector3(), target = new THREE.Vector3(), up = new THREE.Vector3(), m = new THREE.Matrix4();
|
||
window.__applyView = (camera) => {
|
||
if (!window.__view) return;
|
||
eye.fromArray(window.__view.position); target.fromArray(window.__view.target); up.fromArray(window.__view.up ?? [0, 1, 0]);
|
||
m.lookAt(eye, target, up).setPosition(eye);
|
||
camera.matrixWorld.copy(m); camera.matrixWorldInverse.copy(m).invert();
|
||
};
|
||
if (!CSM.prototype.__wrapped) {
|
||
const update = CSM.prototype.update;
|
||
CSM.prototype.update = function (...a) { window.__applyView(this.camera); return update.apply(this, a); };
|
||
CSM.prototype.__wrapped = true;
|
||
}
|
||
```
|
||
|
||
- **Blicke:** Augenhöhe von vorn und von hinten (y 1,7), dazu eine Draufsicht (`position [x, 60, z]`, `up [0, 0, -1]`, Norden oben). Pro Blick `window.__view` setzen und ~1 s warten.
|
||
- **Aufnehmen:** `renderer.render(scene, camera)`, auf ein 2D-Canvas mit 1280 × 800 zeichnen, `toDataURL("image/jpeg", 0.85)` zurückgeben.
|
||
- **Speichern:** Das Ergebnis ist zu groß und landet unter `tool-results/`. `scripts/decode-view.py <tool-result.txt> <bild.jpg>` holt das Bild heraus.
|
||
- **Aufräumen:** `window.__view = null`.
|
||
|
||
## 7. Kollision ablaufen
|
||
|
||
```js
|
||
Object.defineProperty(document, "pointerLockElement", { configurable: true, get: () => renderer.domElement });
|
||
const capsule = scene.getObjectByName("capsule");
|
||
const heading = () => { const d = camera.getWorldDirection(new THREE.Vector3()); return Math.atan2(d.x, -d.z) * 180 / Math.PI; }; // 0 = Norden
|
||
const turn = (deg) => window.dispatchEvent(new MouseEvent("mousemove", { movementX: deg / 8.6 * 300, movementY: 0 }));
|
||
Object.assign(window, { capsule, heading, turn });
|
||
```
|
||
|
||
- **Start:** Kapsel mit `capsule.position.set(x, 1, z)` auf freien Boden des Orts setzen, mit Blick auf eine Wand; der Weg darf nicht über Bordsteine führen. Drehen, `heading()` prüfen, dann `keydown` `KeyW`. Jede Sekunde die Lage notieren, danach `keyup`.
|
||
- **Stand:** Auf blankem Chunk-Boden steht die Kapsel bei y 0,75, auf der Bodenplatte des Orts um deren Oberkante höher (Hood Yard: 0,81).
|
||
- **Wand:** Die Kapsel muss mit wenigen cm Abstand vor der Wand stehen bleiben. Gras und Blätter muss sie ohne Halt durchlaufen.
|
||
- **Tempo:** etwa 0,38 m/s bei 33-ms-Takt, also genug Zeit geben (15–25 s).
|
||
|
||
## 8. Kosten messen
|
||
|
||
`renderer.info.render` zählt die Schattenstufen nicht mit, es wird nach `shadowMap.render` zurückgesetzt. Stattdessen takten, einmal mit und einmal ohne die Wurzel des Orts in der Szene:
|
||
|
||
```js
|
||
const gl = renderer.getContext();
|
||
const measure = (n) => { const t = []; for (let i = 0; i < n; i++) { gl.finish(); const s = performance.now(); renderer.render(scene, camera); gl.finish(); t.push(performance.now() - s); } t.sort((a, b) => a - b); return t[n >> 1]; };
|
||
```
|
||
|
||
- **Median:** Beim Hood Yard kosteten Blick vom Rand und Blick im Ort je +0,5 ms.
|
||
- **Ruckler beim Einsetzen:** Nach einem Sprung die größte Lücke in `window.__frames` rund um den Moment suchen, in dem der Ort in der Szene erscheint. Mit Vorwärmen waren das 86 ms bei 33-ms-Takt, ohne kamen allein ~130 ms für den Texturupload dazu.
|