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:
1 parent
b273bb09cc
commit
ad9bf63b36
2 files changed
+82
-5
No files matched your search
+39
-4
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user