fix(world): quantisierte Positionen vor dem Kollisionsaufbau in Float32 wandeln

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-14 12:42:25 +02:00
1 parent 9f6570b11d
commit 522101c026
3 files changed
+70 -3

No files matched your search

@@ -166,6 +166,8 @@ export const PLACES = [
Durchlässig bleiben Gras (`grass_medium_*`), Blätter und Zweige, die Büsche (`searsia_lucida*`), `contHardware`, `modular_electricity_poles_pieces`, `security_camera_01*` und `trashbag`.
Positionen fester Meshes, die quantisiert vorliegen (KHR_mesh_quantization aus `meshopt()`), wandelt `prepareModel` in Float32 um: `colliderBuild.ts` rechnet `matrixWorld` per `applyMatrix4` in eine Kopie der Geometrie ein, und normalisierte Int16-Werte würden dabei auf ±1 geklemmt.
## Fehlerfälle und Risiken
- **Datei lädt nicht:** `console.warn` mit Ort und Fehler, der Ort gilt als fehlgeschlagen und wird nicht erneut versucht. Das Spiel läuft ohne ihn weiter.
+24 -2
View File
@@ -1,6 +1,7 @@
// Orte: fest platzierte Modelle aus dem Editor three-d-stage, getrennt vom Chunk-Streaming
// (docs/superpowers/specs/2026-09-14-hood-yard-design.md). Aufbereitet werden sie mit
// tools/assets/places.mjs; hier stehen Lage, Laden und Kollision.
import { BufferAttribute } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
import { worldToThree } from "./coords.js";
@@ -79,6 +80,23 @@ export function wantedPlaces(position, places, { show, hide, active }) {
return { add, remove };
}
// tools/assets/places.mjs komprimiert mit meshopt() (KHR_mesh_quantization): Positionen fester Meshes
// liegen als normalisierte Ganzzahlen vor, erst über die Mesh-Transform auf echte Maße skaliert.
// colliderBuild.ts im three-player-controller klont die Geometrie aber und rechnet matrixWorld per
// applyMatrix4 in die Kopie hinein — dabei würden normalisierte Werte beim Zurückschreiben auf ±1
// geklemmt. Deshalb hier vorher in echte, nicht normalisierte Float32-Koordinaten entquantisieren.
function dequantizePosition(geometry) {
const position = geometry.attributes.position;
if (!position.normalized && position.array instanceof Float32Array) return;
const values = new Float32Array(position.count * 3);
for (let i = 0; i < position.count; i++) {
values[i * 3] = position.getX(i);
values[i * 3 + 1] = position.getY(i);
values[i * 3 + 2] = position.getZ(i);
}
geometry.setAttribute("position", new BufferAttribute(values, 3));
}
/**
* Setzt Lage und Schatten und nimmt nicht feste Meshes aus der Kollision (colliderBuild.ts im
* three-player-controller überspringt userData.excludeFromCollider). Gibt zurück, ob ein Teil fest ist.
@@ -93,8 +111,12 @@ export function prepareModel(root, place) {
object.castShadow = true;
object.receiveShadow = true;
const materials = Array.isArray(object.material) ? object.material : [object.material];
if (materials.some((material) => isSolidMaterial(material?.name))) solid = true;
else object.userData.excludeFromCollider = true;
if (materials.some((material) => isSolidMaterial(material?.name))) {
solid = true;
dequantizePosition(object.geometry);
} else {
object.userData.excludeFromCollider = true;
}
});
root.updateMatrixWorld(true);
return solid;
+44 -1
View File
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { BoxGeometry, Group, Mesh, MeshStandardMaterial, Scene, Vector3 } from "three";
import { BoxGeometry, BufferAttribute, BufferGeometry, Group, Mesh, MeshStandardMaterial, Scene,
Vector3 } from "three";
import { PLACES, PlaceLayer, SOLID_MATERIALS, isSolidMaterial, placeTransform, prepareModel,
wantedPlaces } from "../src/world/places.js";
@@ -91,6 +92,48 @@ describe("prepareModel", () => {
it("meldet, wenn kein Teil fest ist", () => {
expect(prepareModel(fakeModel(["grass_medium_01"]), { at: [0, 0], rotation: 0, lift: 0 })).toBe(false);
});
// Bruch: colliderBuild.ts im three-player-controller klont die Geometrie und rechnet matrixWorld
// per applyMatrix4 hinein. Bei einer normalisierten Int16-Position (KHR_mesh_quantization aus
// meshopt() in tools/assets/places.mjs) klemmt das Zurückschreiben die Weltkoordinaten auf ±1 —
// der Hof kollidierte dann nahe dem Weltursprung statt an seiner Stelle im Hood Yard.
it("entquantisiert feste Positionen, bevor der Collider die Weltmatrix hineinrechnet", () => {
const geometry = new BufferGeometry();
geometry.setAttribute("position",
new BufferAttribute(new Int16Array([-32767, 0, -32767, 32767, 32767, 32767]), 3, true));
const mesh = new Mesh(geometry, new MeshStandardMaterial({ name: "cont_a_side" }));
mesh.scale.setScalar(17);
const root = new Group();
root.add(mesh);
prepareModel(root, { at: [1489, 1067.5], rotation: 0, lift: -0.28 });
expect(mesh.geometry.attributes.position.array).toBeInstanceOf(Float32Array);
expect(mesh.geometry.attributes.position.normalized).toBe(false);
// Wie colliderBuild.ts: Geometrie klonen und die Weltmatrix hineinrechnen.
const worldGeometry = mesh.geometry.clone();
worldGeometry.applyMatrix4(mesh.matrixWorld);
worldGeometry.computeBoundingBox();
expect(worldGeometry.boundingBox.min.x).toBeCloseTo(1472);
expect(worldGeometry.boundingBox.max.x).toBeCloseTo(1506);
expect(worldGeometry.boundingBox.min.z).toBeCloseTo(-1084.5);
expect(worldGeometry.boundingBox.max.z).toBeCloseTo(-1050.5);
});
it("lässt eine durchlässige Position quantisiert", () => {
const geometry = new BufferGeometry();
const position = new BufferAttribute(new Int16Array([0, 0, 0, 32767, 32767, 32767]), 3, true);
geometry.setAttribute("position", position);
const mesh = new Mesh(geometry, new MeshStandardMaterial({ name: "grass_medium_01" }));
const root = new Group();
root.add(mesh);
prepareModel(root, { at: [0, 0], rotation: 0, lift: 0 });
expect(mesh.geometry.attributes.position).toBe(position);
});
});
describe("PLACES", () => {