camera fix for large player models
This commit is contained in:
parent
fa459ba8ec
commit
3d518ce3f0
|
|
@ -1,7 +1,7 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import type { ObjectType, RuntimeGameObjectInstance } from "../types";
|
||||
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Euler, Quaternion, Vector3 } from "three";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useKeyboardControls } from "@react-three/drei";
|
||||
|
|
@ -13,7 +13,6 @@ const SPEED = 5;
|
|||
const JUMP_SPEED = 8;
|
||||
const GRAVITY = 20;
|
||||
const SENSITIVITY = 0.002;
|
||||
const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5);
|
||||
const LOOK_RATE = 2000;
|
||||
|
||||
type RapierWorldCreateCharacterControllerFunction = ReturnType<typeof useRapier>['world']['createCharacterController'];
|
||||
|
|
@ -36,6 +35,19 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
|||
const { gl } = useThree();
|
||||
const { world } = useRapier();
|
||||
|
||||
const { shoulderOffset, lookAtY } = useMemo(() => {
|
||||
const bb = object.cache.boundingBox;
|
||||
const W = bb.max[0] - bb.min[0];
|
||||
const H = bb.max[1] - bb.min[1];
|
||||
const D = bb.max[2] - bb.min[2];
|
||||
const radius = Math.sqrt(W * W + H * H + D * D) / 2;
|
||||
const centerY = (bb.min[1] + bb.max[1]) / 2;
|
||||
return {
|
||||
shoulderOffset: new Vector3(W * 0.1, centerY + H * 0.3, bb.max[2] + radius),
|
||||
lookAtY: centerY,
|
||||
};
|
||||
}, [object.id]);
|
||||
|
||||
const yawRef = useRef(0);
|
||||
const pitchRef = useRef(0);
|
||||
const mouseRef = useRef({ x: 0, y: 0 });
|
||||
|
|
@ -128,9 +140,9 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
|||
|
||||
const t = rb.translation();
|
||||
_charPos.set(t.x, t.y, t.z);
|
||||
_offset.copy(SHOULDER_OFFSET).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ'));
|
||||
_offset.copy(shoulderOffset).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ'));
|
||||
camera.position.copy(_charPos).add(_offset);
|
||||
_lookAt.set(t.x, t.y + 1, t.z);
|
||||
_lookAt.set(t.x, t.y + lookAtY, t.z);
|
||||
camera.lookAt(_lookAt);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@
|
|||
&>.title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
& svg {
|
||||
vertical-align: middle;
|
||||
margin: 2px;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
& details {
|
||||
|
|
|
|||
|
|
@ -11,25 +11,26 @@ function v(x: number, y: number, z: number, color: string): Voxel {
|
|||
return { typeId: 'stone', position: [x, y, z], color };
|
||||
}
|
||||
|
||||
// Wolf faces +Z (nose at Z=3). Head spans X:-3..3, Y:0..5, Z:-3..3.
|
||||
// Wolf faces -Z (nose at Z=-3). Head spans X:-3..3, Y:0..5, Z:-3..3.
|
||||
export const wolf: Voxel[] = [
|
||||
// ── Back of skull Z=-3 ──────────────────────────────────────────────
|
||||
v(-1,2,-3,G), v(0,2,-3,G), v(1,2,-3,G),
|
||||
v(-1,3,-3,G), v(0,3,-3,G), v(1,3,-3,G),
|
||||
v(-1,4,-3,G), v(0,4,-3,G), v(1,4,-3,G),
|
||||
// ── Snout tip + nose Z=-3 ────────────────────────────────────────────
|
||||
v(-1,1,-3,L), v(0,1,-3,L), v(1,1,-3,L),
|
||||
v(-1,0,-3,L), v(0,0,-3,L), v(1,0,-3,L),
|
||||
v(-1,2,-3,N), v(0,2,-3,N), v(1,2,-3,N), // nose
|
||||
|
||||
// ── Skull Z=-2 ──────────────────────────────────────────────────────
|
||||
v(-2,2,-2,G), v(-1,2,-2,G), v(0,2,-2,G), v(1,2,-2,G), v(2,2,-2,G),
|
||||
v(-2,3,-2,G), v(-1,3,-2,G), v(0,3,-2,G), v(1,3,-2,G), v(2,3,-2,G),
|
||||
// ── Front face + pupils + snout Z=-2 ────────────────────────────────
|
||||
v(-2,2,-2,L), v(-1,2,-2,L), v(0,2,-2,L), v(1,2,-2,L), v(2,2,-2,L),
|
||||
v(-2,3,-2,G), v(-1,3,-2,N), v(0,3,-2,G), v(1,3,-2,N), v(2,3,-2,G),
|
||||
v(-2,4,-2,G), v(-1,4,-2,G), v(0,4,-2,G), v(1,4,-2,G), v(2,4,-2,G),
|
||||
v(-1,5,-2,G), v(0,5,-2,G), v(1,5,-2,G),
|
||||
v(-1,0,-2,L), v(0,0,-2,L), v(1,0,-2,L),
|
||||
v(-1,1,-2,L), v(0,1,-2,L), v(1,1,-2,L),
|
||||
|
||||
// ── Skull + ear bases Z=-1 ───────────────────────────────────────────
|
||||
v(-2,2,-1,G), v(-1,2,-1,G), v(0,2,-1,G), v(1,2,-1,G), v(2,2,-1,G),
|
||||
v(-2,3,-1,G), v(-1,3,-1,G), v(0,3,-1,G), v(1,3,-1,G), v(2,3,-1,G),
|
||||
v(-3,4,-1,G), v(-2,4,-1,G), v(-1,4,-1,G), v(0,4,-1,G), v(1,4,-1,G), v(2,4,-1,G), v(3,4,-1,G),
|
||||
v(-2,5,-1,G), v(-1,5,-1,G), v(0,5,-1,G), v(1,5,-1,G), v(2,5,-1,G),
|
||||
v(-3,5,-1,D), v(3,5,-1,D),
|
||||
// ── Face + amber eyes + snout Z=-1 ──────────────────────────────────
|
||||
v(-2,2,-1,L), v(-1,2,-1,L), v(0,2,-1,L), v(1,2,-1,L), v(2,2,-1,L),
|
||||
v(-2,3,-1,G), v(-1,3,-1,E), v(0,3,-1,G), v(1,3,-1,E), v(2,3,-1,G),
|
||||
v(-2,4,-1,G), v(-1,4,-1,G), v(0,4,-1,G), v(1,4,-1,G), v(2,4,-1,G),
|
||||
v(-1,0,-1,L), v(0,0,-1,L), v(1,0,-1,L),
|
||||
v(-1,1,-1,L), v(0,1,-1,L), v(1,1,-1,L),
|
||||
|
||||
// ── Face + ears Z=0 ──────────────────────────────────────────────────
|
||||
v(-2,2,0,L), v(-1,2,0,L), v(0,2,0,L), v(1,2,0,L), v(2,2,0,L),
|
||||
|
|
@ -41,22 +42,21 @@ export const wolf: Voxel[] = [
|
|||
v(-1,0,0,L), v(0,0,0,L), v(1,0,0,L),
|
||||
v(-1,1,0,L), v(0,1,0,L), v(1,1,0,L),
|
||||
|
||||
// ── Face + amber eyes + snout Z=1 ────────────────────────────────────
|
||||
v(-2,2,1,L), v(-1,2,1,L), v(0,2,1,L), v(1,2,1,L), v(2,2,1,L),
|
||||
v(-2,3,1,G), v(-1,3,1,E), v(0,3,1,G), v(1,3,1,E), v(2,3,1,G),
|
||||
v(-2,4,1,G), v(-1,4,1,G), v(0,4,1,G), v(1,4,1,G), v(2,4,1,G),
|
||||
v(-1,0,1,L), v(0,0,1,L), v(1,0,1,L),
|
||||
v(-1,1,1,L), v(0,1,1,L), v(1,1,1,L),
|
||||
// ── Skull + ear bases Z=1 ────────────────────────────────────────────
|
||||
v(-2,2,1,G), v(-1,2,1,G), v(0,2,1,G), v(1,2,1,G), v(2,2,1,G),
|
||||
v(-2,3,1,G), v(-1,3,1,G), v(0,3,1,G), v(1,3,1,G), v(2,3,1,G),
|
||||
v(-3,4,1,G), v(-2,4,1,G), v(-1,4,1,G), v(0,4,1,G), v(1,4,1,G), v(2,4,1,G), v(3,4,1,G),
|
||||
v(-2,5,1,G), v(-1,5,1,G), v(0,5,1,G), v(1,5,1,G), v(2,5,1,G),
|
||||
v(-3,5,1,D), v(3,5,1,D),
|
||||
|
||||
// ── Front face + pupils + snout Z=2 ──────────────────────────────────
|
||||
v(-2,2,2,L), v(-1,2,2,L), v(0,2,2,L), v(1,2,2,L), v(2,2,2,L),
|
||||
v(-2,3,2,G), v(-1,3,2,N), v(0,3,2,G), v(1,3,2,N), v(2,3,2,G),
|
||||
// ── Skull Z=2 ───────────────────────────────────────────────────────
|
||||
v(-2,2,2,G), v(-1,2,2,G), v(0,2,2,G), v(1,2,2,G), v(2,2,2,G),
|
||||
v(-2,3,2,G), v(-1,3,2,G), v(0,3,2,G), v(1,3,2,G), v(2,3,2,G),
|
||||
v(-2,4,2,G), v(-1,4,2,G), v(0,4,2,G), v(1,4,2,G), v(2,4,2,G),
|
||||
v(-1,0,2,L), v(0,0,2,L), v(1,0,2,L),
|
||||
v(-1,1,2,L), v(0,1,2,L), v(1,1,2,L),
|
||||
v(-1,5,2,G), v(0,5,2,G), v(1,5,2,G),
|
||||
|
||||
// ── Snout tip + nose Z=3 ─────────────────────────────────────────────
|
||||
v(-1,1,3,L), v(0,1,3,L), v(1,1,3,L),
|
||||
v(-1,0,3,L), v(0,0,3,L), v(1,0,3,L),
|
||||
v(-1,2,3,N), v(0,2,3,N), v(1,2,3,N), // nose
|
||||
// ── Back of skull Z=3 ───────────────────────────────────────────────
|
||||
v(-1,2,3,G), v(0,2,3,G), v(1,2,3,G),
|
||||
v(-1,3,3,G), v(0,3,3,G), v(1,3,3,G),
|
||||
v(-1,4,3,G), v(0,4,3,G), v(1,4,3,G),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export type MenuNodeAction = {
|
|||
|
||||
export type MenuNode = {
|
||||
id: string;
|
||||
title: string;
|
||||
title: ReactNode;
|
||||
className?: string;
|
||||
actions?: MenuNodeAction[];
|
||||
onClick?: () => void;
|
||||
|
|
@ -26,31 +26,40 @@ export class MenuState {
|
|||
}
|
||||
|
||||
private get editorObjectTypesMenu(): MenuNode[] {
|
||||
const scene = state.worldEditor.scene;
|
||||
|
||||
return Object.values(state.world.data.objectTypes)
|
||||
.map((ot) => ({
|
||||
id: `ot-${ot.id}`,
|
||||
title: ot.name,
|
||||
onClick: () => { state.worldEditor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }) },
|
||||
selected: () => state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection?.id === ot.id,
|
||||
children: Object.values(state.worldEditor.scene.objects)
|
||||
children: Object.values(scene.objects)
|
||||
.filter((o) => o.typeId === ot.id)
|
||||
.map((o) => ({
|
||||
id: `o-${o.id}`,
|
||||
title: o.id,
|
||||
className: state.worldEditor.scene.playerObjectId === o.id
|
||||
? 'player-controlled-object'
|
||||
: undefined,
|
||||
actions: [
|
||||
{
|
||||
.map((o) => {
|
||||
const isPlayer = scene.playerObjectId === o.id;
|
||||
|
||||
const actions: MenuNodeAction[] = [];
|
||||
if (!isPlayer)
|
||||
actions.push({
|
||||
id: 'control-by-player',
|
||||
content: <IconRun size="1em" />,
|
||||
tooltip: 'Mark as player',
|
||||
onClick: () => { state.markObjectAsPlayer(o); },
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
id: `o-${o.id}`,
|
||||
title: <>
|
||||
{isPlayer && <IconRun size="1em" />}
|
||||
{o.id}
|
||||
</>,
|
||||
className: isPlayer ? 'player-controlled-object' : undefined,
|
||||
actions,
|
||||
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) },
|
||||
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
|
||||
} as MenuNode))
|
||||
} as MenuNode;
|
||||
})
|
||||
} as MenuNode));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ export class WorldState {
|
|||
},
|
||||
voxelTypes: DEFAULT_VOXEL_TYPES,
|
||||
editorCamera: {
|
||||
position: [-9, 11, 30],
|
||||
look: [-0.52, -0.35, -0.2],
|
||||
position: [14, 17, -25],
|
||||
look: [-2.5, 0.31, 3],
|
||||
},
|
||||
initialScene: {
|
||||
objects: {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export type ObjectInstanceRuntimeData = {
|
|||
cache: {
|
||||
voxelGroups: VoxelGroup[];
|
||||
colliderMesh: [Float32Array, Uint32Array] | null;
|
||||
boundingBox: { min: V3; max: V3 };
|
||||
};
|
||||
pendingActions: {
|
||||
impulse?: { direction: V3, amplitude: number };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,23 @@
|
|||
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
|
||||
import type { ObjectInstance, RuntimeObjectInstance, World } from "../../types";
|
||||
import type { ObjectInstance, ObjectType, RuntimeObjectInstance, V3, World } from "../../types";
|
||||
import { buildObjectTrimesh } from "../graphics/mesh";
|
||||
|
||||
function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
|
||||
if (!objectType.voxels.length)
|
||||
return { min: [0, 0, 0], max: [1, 1, 1] };
|
||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
||||
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
||||
for (const v of objectType.voxels) {
|
||||
minX = Math.min(minX, v.position[0]);
|
||||
minY = Math.min(minY, v.position[1]);
|
||||
minZ = Math.min(minZ, v.position[2]);
|
||||
maxX = Math.max(maxX, v.position[0] + 1);
|
||||
maxY = Math.max(maxY, v.position[1] + 1);
|
||||
maxZ = Math.max(maxZ, v.position[2] + 1);
|
||||
}
|
||||
return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
|
||||
}
|
||||
|
||||
export function populateRuntimeObject(object: ObjectInstance, world: World): RuntimeObjectInstance {
|
||||
const objectType = world.objectTypes[object.typeId];
|
||||
|
||||
|
|
@ -10,12 +26,10 @@ export function populateRuntimeObject(object: ObjectInstance, world: World): Run
|
|||
cache: {
|
||||
voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes),
|
||||
colliderMesh: buildObjectTrimesh(objectType, world.voxelTypes),
|
||||
boundingBox: computeBoundingBox(objectType),
|
||||
},
|
||||
pendingActions: {
|
||||
|
||||
}
|
||||
pendingActions: {},
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance {
|
||||
|
|
|
|||
Loading…
Reference in New Issue