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