fix(props): Straßenlaternen auf glaubwürdige Höhe skaliert, Fuß auf den Boden gehoben

street_lamp_02.glb ist nur 1,657 m hoch (per gltf-transform inspect gemessen:
bboxMax.y 1.26478 - bboxMin.y -0.39258) und ihr Fuß liegt unter dem Ursprung.
#buildLamps setzte das Modell bisher unskaliert auf y=0 — der Fuß steckt im Boden,
und 1,66 m ist ohnehin zu klein neben 10-18 m hohen Palmen und 10-28 m breiten
Straßen dieser Karte. Poly Havens eigene Angabe ("1.7m tall") bestätigt, dass die
Rohgröße korrekt gemessen ist — der Fehler liegt in der Verwendung dieser Rohgröße
in diesem Maßstab, nicht in der Messung selbst.

lampFit(minY, maxY, targetHeight) leitet Skalierung und Fuß-Anhebung aus der
gemessenen bbox her; STREET_LAMP_HEIGHT = 4.0 m als benannte Zielgröße (Mitte des
für dekorative Straßenlaternen üblichen 3-6-m-Bereichs). load() wendet beides auf
die Modellwurzel an, bevor die Teil-Matrizen (Mast, Glas) eingesammelt werden, damit
#buildLamps() unverändert bleiben kann.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-13 05:14:11 +02:00
1 parent b273bb09cc
commit ad9bf63b36
2 files changed
+82 -5

No files matched your search

+39 -4
View File
@@ -1,4 +1,4 @@
import { CylinderGeometry, Group, InstancedMesh, Matrix4, Mesh, MeshStandardMaterial,
import { Box3, CylinderGeometry, Group, InstancedMesh, Matrix4, Mesh, MeshStandardMaterial,
Object3D, RepeatWrapping, SRGBColorSpace, TextureLoader } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
@@ -8,6 +8,31 @@ const PALM_MODEL_HEIGHT = 439.74; // Einheiten im OBJ, gemessen
const TRUNK_RADIUS = 0.22;
const LAMP_VERGE = [6, 8, 10, 12, 14, 16]; // Meter, von innen nach außen (Ruling 60)
// Befund B (Fix-Runde 3): street_lamp_02.glb ist mit `npx @gltf-transform/cli@4.5.0 inspect`
// vermessen nur 1,65736 m hoch (bboxMax.y 1.26478 - bboxMin.y -0.39258) — Poly Haven selbst
// nennt das Modell "1.7m tall" (https://polyhaven.com/a/street_lamp_02), die Messung bestätigt
// also die Rohgröße des Assets. Zu klein ist sie trotzdem: dekorative Straßen-/Parklaternen
// liegen real üblicherweise bei 3-6 m, und neben 10-18 m hohen Palmen (PALM_MODEL_HEIGHT-
// Skalierung oben) und 10-28 m breiten Straßen dieser Karte wirkt 1,66 m spielzeughaft klein.
// 4 m liegt mittig in diesem Bereich und ist mehr als das Doppelte der Rohgröße — eine
// bewusste, keine zufällige Skalierung (anders als bei den Palmen: alle Laternen sind gleich
// hoch, wie ein echtes Serienprodukt).
export const STREET_LAMP_HEIGHT = 4.0; // Meter, Zielhöhe (benannte Konstante, s. Bericht)
/**
* Skalierung und Y-Anhebung, damit ein Modell mit gemessener lokaler bbox [minY, maxY] genau
* targetHeight hoch dasteht und sein tiefster Punkt (der Fuß, minY) nach der Skalierung exakt
* auf y = 0 zu stehen kommt — unabhängig davon, ob der Modellursprung selbst am Fuß liegt.
* street_lamp_02.glb liegt das nicht: bboxMin.y ist negativ, der Fuß also unter dem Ursprung
* (Befund B) — ohne die Anhebung um |bboxMin.y| * scale stünde das skalierte Modell weiterhin
* teilweise im Boden.
*/
export function lampFit(minY, maxY, targetHeight = STREET_LAMP_HEIGHT) {
const modelHeight = maxY - minY;
const scale = modelHeight > 0 ? targetHeight / modelHeight : 1;
return { scale, liftY: -minY * scale };
}
/** Kleiner, wiederholbarer Zufall aus einem Schlüssel. */
function hashUnit(seed, key) {
let h = 2166136261 ^ seed;
@@ -126,6 +151,14 @@ export class PropLayer {
// Referenzcode fehlte das.
gltfLoader.setMeshoptDecoder(MeshoptDecoder);
this.lamp = (await gltfLoader.loadAsync("./models/props/street_lamp_02.glb")).scene;
// Befund B: Skalierung und Fuß-Anhebung auf die Modellwurzel anwenden, BEVOR unten die
// Teil-Matrizen eingesammelt werden — jedes lampParts[i].matrix ist matrixWorld relativ
// zu dieser Wurzel und bekommt scale/liftY dadurch automatisch mit; #buildLamps() selbst
// bleibt unverändert (platziert weiterhin nur bei y = 0 auf dem Boden).
const bbox = new Box3().setFromObject(this.lamp);
const { scale, liftY } = lampFit(bbox.min.y, bbox.max.y);
this.lamp.scale.setScalar(scale);
this.lamp.position.y = liftY;
// Anders als die einteilige Palme hat street_lamp_02.glb zwei Materialien — Metallmast
// und durchsichtiges Laternenglas (glTF-Mesh "street_lamp_02_stem_mesh", primitives:
// [material 1, material 0], per gltf-transform inspect nachgeprüft) — GLTFLoader baut
@@ -187,9 +220,11 @@ export class PropLayer {
/**
* Laternen entlang der Mittellinien (lampPoints), seitlich zur Fahrbahnkante versetzt
* (vergeLampPoints, Ruling 60 — Fix-Runde 1; siehe dort für die Begründung). Das Modell
* ist bei 1,7 m laut Poly-Haven-Angabe bereits maßstäblich, anders als die Palme also
* keine Zufallsskalierung.
* (vergeLampPoints, Ruling 60 — Fix-Runde 1; siehe dort für die Begründung). Skalierung
* auf STREET_LAMP_HEIGHT und die Fuß-Anhebung stecken bereits in this.lampParts[*].matrix
* (s. load(), Befund B / Fix-Runde 3) — hier wird jede Laterne nur noch bei y = 0 auf den
* Boden gesetzt. Anders als bei der Palme keine Zufallsskalierung je Instanz: jede Laterne
* ist gleich hoch, wie ein echtes Serienprodukt.
*/
#buildLamps(chunk, visual) {
const centerlinePoints = lampPoints(chunk.centerlines ?? []);
+43 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { lampPoints, palmTransforms, pointInPolygon, vergeLampPoints } from "../src/world/props.js";
import { STREET_LAMP_HEIGHT, lampFit, lampPoints, palmTransforms, pointInPolygon, vergeLampPoints } from "../src/world/props.js";
describe("props", () => {
it("setzt Laternen im gewünschten Abstand entlang der Linie", () => {
@@ -105,3 +105,45 @@ describe("vergeLampPoints", () => {
expect(out[0].z).toBeCloseTo(6);
});
});
// Fix-Runde 3, Befund B: street_lamp_02.glb ist mit `npx @gltf-transform/cli@4.5.0 inspect`
// vermessen bboxMin.y=-0.39258 / bboxMax.y=1.26478 (Modellhöhe 1,65736 m) — der Fuß liegt
// unter dem Ursprung, #buildLamps setzte das Modell bisher unskaliert auf y=0, der Fuß steckt
// also im Boden, und 1,66 m ist ohnehin zu klein für eine Straßenlaterne (s. Bericht).
// lampFit() ist rein: sie bekommt die gemessene bbox und die Zielhöhe hinein, keine
// Seiteneffekte, kein GLTFLoader — PropLayer.load() ist der einzige Aufrufer.
describe("lampFit", () => {
const BBOX_MIN_Y = -0.39258;
const BBOX_MAX_Y = 1.26478;
it("skaliert die gemessene Modellhöhe exakt auf die Zielhöhe", () => {
const { scale } = lampFit(BBOX_MIN_Y, BBOX_MAX_Y, STREET_LAMP_HEIGHT);
const modelHeight = BBOX_MAX_Y - BBOX_MIN_Y;
expect(scale * modelHeight).toBeCloseTo(STREET_LAMP_HEIGHT, 5);
});
it("hebt das skalierte Modell so an, dass sein Fuß (bboxMin.y) auf y = 0 zu stehen kommt", () => {
const { scale, liftY } = lampFit(BBOX_MIN_Y, BBOX_MAX_Y, STREET_LAMP_HEIGHT);
expect(BBOX_MIN_Y * scale + liftY).toBeCloseTo(0, 5);
});
it("STREET_LAMP_HEIGHT ist eine glaubwürdige Straßenlaternenhöhe, keine 1,66 m", () => {
// 3-6 m ist der in Quellen genannte Bereich für dekorative/ornamentale Straßen- und
// Parklaternen (s. Bericht); das Modell selbst ist mit 1,66 m unstrittig zu klein.
expect(STREET_LAMP_HEIGHT).toBeGreaterThanOrEqual(3);
expect(STREET_LAMP_HEIGHT).toBeLessThanOrEqual(6);
});
it("leitet den Skalierungsfaktor aus der übergebenen Zielhöhe her, statt eine Zahl zu raten", () => {
expect(lampFit(0, 2, 4).scale).toBeCloseTo(2);
expect(lampFit(0, 4, 4).scale).toBeCloseTo(1);
expect(lampFit(0, 1, 4).scale).toBeCloseTo(4);
});
it("liefert scale 1 und hebt nur um |bboxMin.y| an, wenn keine Zielhöhe übergeben wird und die Modellhöhe 0 ist (Randfall)", () => {
// Entartete bbox (maxY === minY) darf nicht durch 0 teilen.
const { scale, liftY } = lampFit(2, 2, STREET_LAMP_HEIGHT);
expect(scale).toBe(1);
expect(liftY).toBeCloseTo(-2);
});
});