feat(world): Orte beim Vorladen vorwärmen

PlaceLayer nimmt jetzt ein optionales warmUp(root) entgegen und ruft es
nach prepareModel, aber vor dem Einsetzen auf. main.js nutzt das, um
Texturen hochzuladen und CSM-Shader zu übersetzen, solange der Ort noch
nicht in der Szene ist — gemessen kostete das sonst rund 130 ms auf
einen Schlag im ersten Bild, in dem der Hof erscheint. Schlägt das
Vorwärmen fehl, wird nur gewarnt; der Ort wird trotzdem eingesetzt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-14 13:39:50 +02:00
1 parent 522101c026
commit 1c24fbf71e
3 files changed
+68 -7

No files matched your search

+21 -2
View File
@@ -6,7 +6,7 @@ import { playerController } from "three-player-controller";
import { ChunkStreamer } from "./world/streaming.js";
import { createMaterials } from "./world/materials.js";
import { createSky } from "./world/sky.js";
import { createSunShadows } from "./world/shadows.js";
import { createSunShadows, prepareLitMaterials } from "./world/shadows.js";
import { PropLayer } from "./world/props.js";
import { PlaceLayer } from "./world/places.js";
import { chunkForWorld, threeToWorld } from "./world/coords.js";
@@ -237,9 +237,28 @@ await props.load();
// endlose Fall-Auffang-Schleife laufen, sobald der Chunk dauerhaft ausbleibt. Nach 15s gibt
// es nur noch eine einmalige Warnung, kein Verhaltenswechsel.
const streamer = new ChunkStreamer({ scene, player, materials, props, pinned: VEHICLE_PINNED_CHUNKS });
// warmUp fürs Vorladen eines Orts (PlaceLayer unten): lädt seine Texturen schon hier auf die GPU und
// übersetzt seine CSM-Shader, statt das im ersten Bild nach dem Einsetzen zu tun — dort kostete genau
// das gemessen rund 130 ms auf einmal (docs/superpowers/specs/2026-09-14-hood-yard-design.md,
// "Fehlerfälle und Risiken").
async function warmUpPlace(root) {
prepareLitMaterials(root, sun.csm);
const textures = new Set();
root.traverse((object) => {
if (!object.isMesh) return;
for (const material of Array.isArray(object.material) ? object.material : [object.material]) {
if (!material) continue;
for (const value of Object.values(material)) {
if (value?.isTexture) textures.add(value);
}
}
});
for (const texture of textures) renderer.initTexture(texture);
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.
const places = new PlaceLayer({ scene, player });
const places = new PlaceLayer({ scene, player, warmUp: warmUpPlace });
player.setSkipCapsuleCollision(true);
let settled = false;
let warnedSlowSpawn = false;
+12 -3
View File
@@ -130,8 +130,8 @@ function loadModel(url) {
}
export class PlaceLayer {
constructor({ scene, player, places = PLACES, load = loadModel }) {
Object.assign(this, { scene, player, places, load });
constructor({ scene, player, places = PLACES, load = loadModel, warmUp = null }) {
Object.assign(this, { scene, player, places, load, warmUp });
this.models = new Map(); // id -> { root, solid }; bleibt nach dem ersten Laden erhalten
this.pending = new Set();
this.failed = new Set();
@@ -161,9 +161,18 @@ export class PlaceLayer {
if (this.models.has(id) || this.pending.has(id) || this.failed.has(id)) return;
this.pending.add(id);
this.load(place.url)
.then((root) => {
.then(async (root) => {
const solid = prepareModel(root, place);
if (!solid) console.warn(`Ort ${id} hat keine festen Teile und bekommt keine Kollision.`);
// Vorwärmen (Texturupload, Shader-Übersetzung) noch hier im Vorladen: schlägt es fehl,
// wird der Ort trotzdem eingesetzt, nur ohne den Vorteil (siehe main.js, warmUpPlace).
if (this.warmUp) {
try {
await this.warmUp(root);
} catch (err) {
console.warn(`Ort ${id} konnte nicht vorgewärmt werden:`, err);
}
}
this.models.set(id, { root, solid });
// Nur einsetzen, wenn der Spieler noch da ist; sonst beim nächsten Herankommen.
if (this.wanted.has(id)) this.#show(id);