fix(showroom): Funde der Schlussdurchsicht auf der Testseite behoben
Die Spielfigur wirft jetzt einen Schatten (player.init durchlief applyCharacter nie); drei stille Fehlerpfade meckern jetzt statt roh abzubrechen (fehlender Clip fuer die Fingerhaltung, Waffe ohne Griff, mit neuem Test); die Waffenauswahl im Panel zeigt Beschriftungen statt IDs; der Waffen-Umbau prueft jetzt die Meshzahl je Datei; stageTransform ist aus main.js nach lineup.js gezogen und mit dem im Browser gemessenen Fall getestet; slotPosition nimmt DUMMIES als Argument statt es aus dem Modul zu lesen; dazu Kommentar- und Entwurfskorrekturen (Docstring, Beckenspur-Test, Frustum-Culling, Port/Kamera/ VIEW_DISTANCE im Entwurf). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8f1832eae
commit
a0cd95cceb
9 files changed
+97
-18
No files matched your search
@@ -37,7 +37,9 @@ export function createDummy({ template, clip, fingers, weapon = null, hand = "r"
|
||||
if (!object.isMesh) return;
|
||||
object.castShadow = true;
|
||||
object.receiveShadow = true;
|
||||
// Der Bogenschütze wandert bis 3 m von seinem Platz; die Hülle der Ruhepose schnitte ihn am Bildrand weg.
|
||||
// Für ein Skinned Mesh ist die Hülle der Ruhepose ohnehin die falsche Grundlage, sobald Knochen die
|
||||
// Figur bewegen — besonders sichtbar beim Bogenschützen, der bis 3 m von seinem Platz wandert. Deshalb
|
||||
// für alle Dummies aus, nicht nur für ihn.
|
||||
object.frustumCulled = false;
|
||||
});
|
||||
const mixer = new AnimationMixer(root);
|
||||
@@ -46,6 +48,7 @@ export function createDummy({ template, clip, fingers, weapon = null, hand = "r"
|
||||
|
||||
let held = null;
|
||||
if (weapon) {
|
||||
if (!grip) throw new Error("Dummy mit Waffe ohne Griff");
|
||||
const boneName = hand === "l" ? "hand_l" : "hand_r";
|
||||
const bone = root.getObjectByName(boneName);
|
||||
if (!bone) throw new Error(`Dummy ohne Knochen ${boneName}`);
|
||||
|
||||
+14
-2
@@ -47,12 +47,24 @@ export const DUMMIES = [
|
||||
];
|
||||
|
||||
/** Platz eines Dummys in Mannequin-Metern. Der Bogenschütze steht abseits, er wandert bis 3,13 m. */
|
||||
export function slotPosition(index) {
|
||||
export function slotPosition(index, dummies = DUMMIES) {
|
||||
if (index < 8) return { x: FIRST_SLOT_X + ROW_SPACING * index, z: 0 };
|
||||
if (DUMMIES[index]?.clip.startsWith("hunter_bowArrow")) return { x: BOW_X, z: -ROW_DEPTH };
|
||||
if (dummies[index]?.clip.startsWith("hunter_bowArrow")) return { x: BOW_X, z: -ROW_DEPTH };
|
||||
return { x: FIRST_SLOT_X + ROW_SPACING * (index - 8), z: -ROW_DEPTH };
|
||||
}
|
||||
|
||||
/**
|
||||
* Lage der Aufstellung: VIEW_DISTANCE Mannequin-Meter vor der Figur in Blickrichtung, +Z zurück zu ihr gedreht.
|
||||
* position ist die Figurenposition (x, z genügen), view die bereits normalisierte waagrechte Blickrichtung.
|
||||
*/
|
||||
export function stageTransform(position, view, scale) {
|
||||
return {
|
||||
x: position.x + view.x * VIEW_DISTANCE * scale,
|
||||
z: position.z + view.z * VIEW_DISTANCE * scale,
|
||||
rotationY: Math.atan2(-view.x, -view.z),
|
||||
};
|
||||
}
|
||||
|
||||
/** Text des Schilds: Clip oben, Waffe darunter. */
|
||||
export function labelLines(dummy) {
|
||||
return [dummy.label, dummy.weapon ? WEAPONS[dummy.weapon].label : "ohne Waffe"];
|
||||
|
||||
+21
-7
@@ -14,7 +14,7 @@ import { createSky } from "../world/sky.js";
|
||||
import { createSunShadows } from "../world/shadows.js";
|
||||
import { createDummy, fingerPoseClip } from "./dummies.js";
|
||||
import { createLabel } from "./labels.js";
|
||||
import { DUMMIES, labelLines, LABEL_HEIGHT, MOTIONS_URL, slotPosition, VIEW_DISTANCE, WEAPONS } from "./lineup.js";
|
||||
import { DUMMIES, labelLines, LABEL_HEIGHT, MOTIONS_URL, slotPosition, stageTransform, WEAPONS } from "./lineup.js";
|
||||
import { createShowroomPanel } from "./panel.js";
|
||||
|
||||
// Dieselben Werte wie in src/main.js (dort lokale Konstanten): Sonne in gemessener Stärke, Himmelslicht und
|
||||
@@ -81,6 +81,14 @@ await player.init({
|
||||
playerModelConfig: { ...PLAYER_MODEL_BASE, model: ual.scene, animations: ual.animations },
|
||||
colliders: [{ motion: "static", shape: { kind: "mesh", source: ground } }],
|
||||
});
|
||||
// Wie in createCharacterModule (src/characters/index.js): Der Controller setzt beim Laden selbst keinen
|
||||
// Schatten, und dieses Startmodell durchläuft applyCharacter nie — ohne diese Schleife bliebe die Spielfigur
|
||||
// als einzige Figur der Seite ohne Schatten.
|
||||
player.playerModel.traverse((object) => {
|
||||
if (!object.isMesh) return;
|
||||
object.castShadow = true;
|
||||
object.receiveShadow = true;
|
||||
});
|
||||
// Der Controller skaliert das Modell selbst (playerCapsuleHeight × scale). Ausgelesen statt nachgerechnet:
|
||||
// daran hängen Dummies, Waffen, Schilder und alle Abstände.
|
||||
const figureScale = player.playerModel.getWorldScale(new Vector3()).x;
|
||||
@@ -91,8 +99,15 @@ stage.scale.setScalar(figureScale);
|
||||
scene.add(stage);
|
||||
|
||||
const clips = new Map(motions.animations.map((clip) => [clip.name, clip]));
|
||||
const gripFingers = fingerPoseClip(AnimationClip.findByName(ual.animations, "Pistol_Idle_Loop"), "Finger_Waffe");
|
||||
const looseFingers = fingerPoseClip(AnimationClip.findByName(ual.animations, "Idle_Loop"), "Finger_locker");
|
||||
// Wie beim Clip-Lookup der Dummies unten: findByName gibt bei einem fehlenden Clip still null zurück, das
|
||||
// bräche erst tief in fingerPoseClip mit einem unklaren TypeError ab.
|
||||
function requireAnimation(name) {
|
||||
const clip = AnimationClip.findByName(ual.animations, name);
|
||||
if (!clip) throw new Error(`ual.glb ohne Clip ${name}`);
|
||||
return clip;
|
||||
}
|
||||
const gripFingers = fingerPoseClip(requireAnimation("Pistol_Idle_Loop"), "Finger_Waffe");
|
||||
const looseFingers = fingerPoseClip(requireAnimation("Idle_Loop"), "Finger_locker");
|
||||
const weaponScenes = new Map(weaponIds.map((id, index) => [id, weaponFiles[index].scene]));
|
||||
const weaponObjects = new Map();
|
||||
const dummies = DUMMIES.map((entry, index) => {
|
||||
@@ -130,10 +145,9 @@ function placeStage() {
|
||||
view.y = 0;
|
||||
if (view.lengthSq() < 1e-6) return false;
|
||||
view.normalize();
|
||||
const position = player.playerCapsule.position;
|
||||
stage.position.set(
|
||||
position.x + view.x * VIEW_DISTANCE * figureScale, 0, position.z + view.z * VIEW_DISTANCE * figureScale);
|
||||
stage.rotation.y = Math.atan2(-view.x, -view.z);
|
||||
const { x, z, rotationY } = stageTransform(player.playerCapsule.position, view, figureScale);
|
||||
stage.position.set(x, 0, z);
|
||||
stage.rotation.y = rotationY;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ export function createShowroomPanel({ playback, weaponObjects }) {
|
||||
|
||||
const folder = gui.addFolder("Griff");
|
||||
const ids = Object.keys(WEAPONS);
|
||||
// lil-gui zeigt bei einem Objekt { Beschriftung: Wert } die Schlüssel statt der IDs im Dropdown an.
|
||||
const weaponOptions = Object.fromEntries(ids.map((id) => [WEAPONS[id].label, id]));
|
||||
const edit = { weapon: ids[0], x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0 };
|
||||
const sliders = [];
|
||||
const load = () => {
|
||||
@@ -29,7 +31,7 @@ export function createShowroomPanel({ playback, weaponObjects }) {
|
||||
grip.rotation = [edit.rx, edit.ry, edit.rz];
|
||||
for (const object of weaponObjects.get(edit.weapon) ?? []) applyGrip(object, grip);
|
||||
};
|
||||
folder.add(edit, "weapon", ids).name("Waffe").onChange(load);
|
||||
folder.add(edit, "weapon", weaponOptions).name("Waffe").onChange(load);
|
||||
for (const axis of ["x", "y", "z"]) {
|
||||
sliders.push(folder.add(edit, axis, -0.3, 0.3, 0.001).name(`Position ${axis} (m)`).onChange(apply));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user