fix(world): quantisierte Positionen vor dem Kollisionsaufbau in Float32 wandeln
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
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
@@ -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
@@ -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", () => {
|
||||
|
||||
Reference in new issue
Block a user