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>
7.8 KiB
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.jsondes 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.
{ "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, dazuthree-player-controller/package.jsonunddist/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:
<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 ??=.
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):
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.traversedie Meshes ohneuserData.excludeFromCollidersammeln. Ihre Materialnamen müssen genau die festen Teile sein. - Quantisierung: Alle festen Meshes haben
geometry.attributes.position.array instanceof Float32Array, dafür sorgtprepareModel. 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.texturesliegt 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:
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 Blickwindow.__viewsetzen 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
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, dannkeydownKeyW. Jede Sekunde die Lage notieren, danachkeyup. - 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:
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.__framesrund 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.