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`.
|
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
|
## 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.
|
- **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
|
// 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
|
// (docs/superpowers/specs/2026-09-14-hood-yard-design.md). Aufbereitet werden sie mit
|
||||||
// tools/assets/places.mjs; hier stehen Lage, Laden und Kollision.
|
// tools/assets/places.mjs; hier stehen Lage, Laden und Kollision.
|
||||||
|
import { BufferAttribute } from "three";
|
||||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
||||||
import { worldToThree } from "./coords.js";
|
import { worldToThree } from "./coords.js";
|
||||||
@@ -79,6 +80,23 @@ export function wantedPlaces(position, places, { show, hide, active }) {
|
|||||||
return { add, remove };
|
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
|
* 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.
|
* 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.castShadow = true;
|
||||||
object.receiveShadow = true;
|
object.receiveShadow = true;
|
||||||
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
||||||
if (materials.some((material) => isSolidMaterial(material?.name))) solid = true;
|
if (materials.some((material) => isSolidMaterial(material?.name))) {
|
||||||
else object.userData.excludeFromCollider = true;
|
solid = true;
|
||||||
|
dequantizePosition(object.geometry);
|
||||||
|
} else {
|
||||||
|
object.userData.excludeFromCollider = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
root.updateMatrixWorld(true);
|
root.updateMatrixWorld(true);
|
||||||
return solid;
|
return solid;
|
||||||
|
|||||||
+44
-1
@@ -1,5 +1,6 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
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,
|
import { PLACES, PlaceLayer, SOLID_MATERIALS, isSolidMaterial, placeTransform, prepareModel,
|
||||||
wantedPlaces } from "../src/world/places.js";
|
wantedPlaces } from "../src/world/places.js";
|
||||||
|
|
||||||
@@ -91,6 +92,48 @@ describe("prepareModel", () => {
|
|||||||
it("meldet, wenn kein Teil fest ist", () => {
|
it("meldet, wenn kein Teil fest ist", () => {
|
||||||
expect(prepareModel(fakeModel(["grass_medium_01"]), { at: [0, 0], rotation: 0, lift: 0 })).toBe(false);
|
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", () => {
|
describe("PLACES", () => {
|
||||||
|
|||||||
Reference in new issue
Block a user