layered three view

game object geometries layer
This commit is contained in:
azykov@mail.ru
2026-06-19 20:54:27 +03:00
parent 076cded364
commit 579fe36e6c
31 changed files with 521 additions and 219 deletions
+2 -2
View File
@@ -2,13 +2,13 @@ import { Html } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useRef } from "react";
import * as THREE from "three";
import type { RuntimeObjectInstance } from "../types";
import { debugLabelsStore } from "../state/debugLabelsState";
import { state } from "../state";
import { observer } from "mobx-react-lite";
import { formatV3 } from "../utils";
import type { ObjectInstance } from "../types";
export const DebugLabel = observer(function ({ obj }: { obj: RuntimeObjectInstance; }) {
export const DebugLabel = observer(function ({ obj }: { obj: ObjectInstance; }) {
const groupRef = useRef<THREE.Group>(null);
const rb = obj.rb();
+1 -2
View File
@@ -2,9 +2,8 @@ import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { debugLabelsStore } from "../state/debugLabelsState";
import { DebugLabel } from "./DebugLabel";
import type { RuntimeScene } from "../types";
export const DebugLabelsView = observer(function ({ scene, editMode }: { scene: RuntimeScene, editMode?: boolean }) {
export const DebugLabelsView = observer(function ({ scene, editMode }: { scene: Scene, editMode?: boolean }) {
useEffect(
() => {
return () => { debugLabelsStore.clear(); };
+2 -3
View File
@@ -1,11 +1,10 @@
import { observer } from "mobx-react-lite";
import type { RuntimeGameObjectInstance } from "../types";
import { state } from "../state";
import { ObjectViewInternal } from "./ObjectViewInternal";
import { PlayerObjectView } from "./CharacterView";
export type GameObjectViewProps = {
object: RuntimeGameObjectInstance;
object: ObjectInstance;
isPlayer: boolean;
isSceneEditor?: boolean;
}
@@ -16,7 +15,7 @@ export const GameObjectView = observer(function (props: GameObjectViewProps) {
return null;
if (props.isPlayer)
return <PlayerObjectView object={props.object as RuntimeGameObjectInstance} objectType={objectType} />;
return <PlayerObjectView object={props.object} objectType={objectType} />;
function handleClick(): void {
state.game?.emitEvent('objectClicked', { object: props.object });
@@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite";
import { runInAction } from "mobx";
import type { ObjectInstance, R3, Runtime } from "../types";
import type { ObjectInstance, R3 } from "../types";
import { useMemo, useRef, useEffect, useCallback, type RefObject } from "react";
import type { Group } from "three";
import { Euler, Quaternion, Vector3, BoxHelper, type LineBasicMaterial } from "three";
@@ -11,7 +11,7 @@ import { nextObjectEditMode } from "../state/worldEditorState";
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
type ObjectEditorViewProps = {
object: Runtime<ObjectInstance>;
object: ObjectInstance;
isPlayer: boolean;
}
+13 -15
View File
@@ -1,4 +1,4 @@
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
import type { ObjectInstance, ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
import type { VoxelGroup } from "../types/runtime/voxelGroup";
import { useEffect, useImperativeHandle, useRef, type Ref, memo, useMemo } from "react";
import { Euler, Quaternion, Mesh, MeshStandardMaterial, type Group } from "three";
@@ -8,10 +8,10 @@ import { SyncRigidBody } from "./SyncRigidBody";
import { reaction, runInAction } from "mobx";
import { v3toRapier } from "../utils";
import { state } from "../state";
import { buildVoxelMesh } from "../utils/graphics/voxelMesh";
import { buildVoxelGroupMesh } from "../utils/graphics/voxelMesh";
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg, opacity }: { vg: VoxelGroup, opacity: number }) {
const geo = useMemo(() => buildVoxelMesh(vg.positions), [vg]);
const geo = useMemo(() => buildVoxelGroupMesh(vg.positions), [vg]);
useEffect(() => () => { geo.dispose(); }, [geo]);
const op = vg.opacity * opacity;
@@ -29,7 +29,7 @@ export type ObjectViewInternalHandle = {
}
type ObjectViewInternalProps = {
object: Omit<RuntimeObjectInstance, 'typeId'>;
object: Omit<ObjectInstance, 'typeId'>;
objectType: ObjectType;
editMode?: boolean;
isPlayer?: boolean;
@@ -54,19 +54,17 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
if (!rbRef.current)
return;
const gameObj = object as RuntimeGameObjectInstance;
rbRef.current.setTranslation(v3toRapier(object.position), true);
rbRef.current.setTranslation(v3toRapier(gameObj.position), true);
const euler = new Euler(gameObj.rotation[0], gameObj.rotation[1], gameObj.rotation[2]);
const euler = new Euler(...object.rotation);
const q = new Quaternion().setFromEuler(euler);
rbRef.current.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true);
groupRef.current?.scale.set(...gameObj.scale);
groupRef.current?.scale.set(...object.scale);
rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true);
rbRef.current.setLinvel(v3toRapier(object.linearVelocity), true);
rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true);
rbRef.current.setAngvel(v3toRapier(object.angularVelocity), true);
},
);
},
@@ -141,8 +139,8 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
object.position = data.position;
object.rotation = data.rotation;
if (!props.editMode) {
(object as RuntimeGameObjectInstance).linearVelocity = data.linearVelocity;
(object as RuntimeGameObjectInstance).angularVelocity = data.radialVelocity;
(object as ObjectInstance).linearVelocity = data.linearVelocity;
(object as ObjectInstance).angularVelocity = data.radialVelocity;
}
// do not change object.version to not trigger rerender
});
@@ -154,7 +152,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
scale={object.scale}
onClick={handleClick}
>
{
{/* {
object.cache.voxelGroups.map((vg) =>
<VoxelGroupMesh
key={`${vg.id}-${object.cache.updatedAt}`}
@@ -163,7 +161,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
/>
)
}
{object.cache.colliderMesh && <ConvexHullCollider args={[object.cache.colliderMesh[0]]} />}
{object.cache.colliderMesh && <ConvexHullCollider args={[object.cache.colliderMesh[0]]} />} */}
</group>
</SyncRigidBody>
);
+1 -1
View File
@@ -1,6 +1,6 @@
import { Suspense, useRef } from 'react';
import type React from 'react';
import { Grid, OrbitControls } from '@react-three/drei';
import { OrbitControls } from '@react-three/drei';
import { observer } from 'mobx-react-lite';
import { state } from '../state';
import { SceneView } from './SceneView';
+1 -1
View File
@@ -1,5 +1,5 @@
import { observer } from "mobx-react-lite";
import type { RuntimeGameObjectInstance, RuntimeScene } from "../types";
import type { RuntimeScene } from "../types";
import { GameObjectView } from "./GameObjectView";
import { ObjectEditorView } from "./ObjectEditorView";
import { DebugLabelsView } from "./DebugLabelsView";
@@ -0,0 +1,5 @@
import { observer } from "mobx-react-lite";
export const DebugLabelsLayer = observer(function () {
return null; //TODO
});
@@ -0,0 +1,27 @@
import { useFrame } from "@react-three/fiber";
import type { Pos3 } from "../../types";
import { Euler, Quaternion, Vector3 } from "three";
const _q = new Quaternion();
const _e = new Euler(0, 0, 0, 'YXZ');
const _offset = new Vector3();
const _charPos = new Vector3();
const _lookAt = new Vector3();
type Props = {
camera: Pos3;
};
export function CameraLayer({ camera: cameraPos }: Props) {
useFrame(({ camera }) => {
// const t = rb.translation();
// _charPos.set(t.x, t.y, t.z);
// _offset.copy(shoulderOffset).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ'));
// camera.position.copy(_charPos).add(_offset);
// _lookAt.set(t.x, t.y + lookAtY, t.z);
camera.lookAt(_lookAt);
});
return null;
}
@@ -0,0 +1,23 @@
import { observer } from "mobx-react-lite";
import { ObjectGeomertiesLayer } from "./ObjectGeomertiesLayer";
import { Canvas } from "@react-three/fiber";
import { Perf } from "r3f-perf";
import { state } from "../../state";
import { CameraLayer } from "./GameraLayer";
export const LayeredThreeView = observer(function () {
return <>
<Canvas
// camera={state.world.character.camera}
>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} />
<Perf position="bottom-right" />
<CameraLayer camera={state.worldEditor.camera} />
<ObjectGeomertiesLayer />
</Canvas>
</>;
});
@@ -0,0 +1,26 @@
import { useEffect, useRef } from "react";
import { useThree, useFrame } from "@react-three/fiber";
import { ObjectGeometryList } from "../../rendering/ObjectGeometryList";
import { observer } from "mobx-react-lite";
import { state } from "../../state";
export const ObjectGeomertiesLayer = observer(function () {
const geometries = state.view.objectGeometries;
const threeScene = useThree(s => s.scene);
const list = useRef<ObjectGeometryList | null>(null);
useEffect(() => {
const l = new ObjectGeometryList(threeScene, geometries);
list.current = l;
return () => {
l.dispose();
list.current = null;
};
}, [threeScene, geometries]);
useFrame(() => list.current?.tick());
return null;
});
@@ -1,11 +1,11 @@
import { Canvas } from '@react-three/fiber';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useNavigate, Link, useParams } from 'react-router-dom';
import { observer } from 'mobx-react-lite';
import { useState, useCallback, useEffect, useRef } from 'react';
import { state } from '../../state';
import type { Voxel, V3 } from '../../types';
import { VoxelEditorScene } from './VoxelEditorScene';
import { IconArrowLeft, IconArrowBackUp, IconArrowForwardUp, IconPlus, IconTrash } from '@tabler/icons-react';
import { state } from '../../state';
const PALETTE = [
'#ffffff', '#cccccc', '#999999', '#555555', '#222222',
@@ -19,7 +19,10 @@ type HistoryEntry =
| { type: 'remove'; voxel: Voxel };
export const VoxelEditorPage = observer(function () {
const { id } = useParams<{ id: string }>();
const objectType = id ? state.view.objectTypeGeometries.get(id) : undefined;
const navigate = useNavigate();
const [mode, setMode] = useState<'add' | 'remove'>('add');
const [color, setColor] = useState('#808080');
@@ -28,14 +31,11 @@ export const VoxelEditorPage = observer(function () {
const history = useRef<HistoryEntry[]>([]);
const future = useRef<HistoryEntry[]>([]);
const objectType = id ? state.world.getObjectTypeById(id) : undefined;
useEffect(() => {
return () => {
if (id)
state.worldEditor.refreshObjectTypeCaches(id);
objectType?.updateRuntimeData();
};
}, [id]);
}, [objectType]);
function pushHistory(entry: HistoryEntry) {
history.current.push(entry);
@@ -46,31 +46,31 @@ state.worldEditor.refreshObjectTypeCaches(id);
const undo = useCallback(() => {
const entry = history.current.pop();
if (!entry || !id)
if (!entry)
return;
if (entry.type === 'add') {
state.worldEditor.removeVoxelFromObjectType(id, entry.voxel.position);
objectType?.removeVoxel(entry.voxel.position);
} else {
state.worldEditor.addVoxelToObjectType(id, entry.voxel);
objectType?.addVoxel(entry.voxel);
}
future.current.push(entry);
setCanUndo(history.current.length > 0);
setCanRedo(true);
}, [id]);
}, [objectType]);
const redo = useCallback(() => {
const entry = future.current.pop();
if (!entry || !id)
if (!entry)
return;
if (entry.type === 'add') {
state.worldEditor.addVoxelToObjectType(id, entry.voxel);
objectType?.addVoxel(entry.voxel);
} else {
state.worldEditor.removeVoxelFromObjectType(id, entry.voxel.position);
objectType?.removeVoxel(entry.voxel.position);
}
history.current.push(entry);
setCanUndo(true);
setCanRedo(future.current.length > 0);
}, [id]);
}, [objectType]);
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
@@ -84,24 +84,14 @@ state.worldEditor.refreshObjectTypeCaches(id);
}, [undo, redo]);
function handleAdd(voxel: Voxel) {
if (!id)
return;
state.worldEditor.addVoxelToObjectType(id, voxel);
pushHistory({ type: 'add', voxel });
if (objectType?.addVoxel(voxel))
pushHistory({ type: 'add', voxel });
}
function handleRemove(position: V3) {
if (!id || !objectType)
return;
const voxel = objectType.voxels.find(v =>
v.position[0] === position[0] &&
v.position[1] === position[1] &&
v.position[2] === position[2]
);
if (!voxel)
return;
state.worldEditor.removeVoxelFromObjectType(id, position);
pushHistory({ type: 'remove', voxel });
const voxel = objectType?.removeVoxel(position);
if (voxel)
pushHistory({ type: 'remove', voxel });
}
if (!objectType) {
@@ -116,11 +106,11 @@ state.worldEditor.refreshObjectTypeCaches(id);
<div style={{ position: 'fixed', inset: 0, background: '#1a1a2e', display: 'flex', flexDirection: 'column' }}>
{/* Top bar */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', background: '#11111f', flexShrink: 0 }}>
<button onClick={() => navigate(`/editor/objectType/${id}`)} style={btnStyle}>
<button onClick={() => navigate(`/editor/objectType/${objectType.objectType.id}`)} style={btnStyle}>
<IconArrowLeft size={20} />
</button>
<span style={{ color: '#ccc', fontWeight: 600 }}>{objectType.name}</span>
<span style={{ color: '#666', fontSize: 13 }}>{objectType.voxels.length} voxels</span>
<span style={{ color: '#ccc', fontWeight: 600 }}>{objectType.objectType.name}</span>
<span style={{ color: '#666', fontSize: 13 }}>{objectType.objectType.voxels.length} voxels</span>
<div style={{ flex: 1 }} />
<button onClick={undo} disabled={!canUndo} style={{ ...btnStyle, opacity: canUndo ? 1 : 0.35 }}>
<IconArrowBackUp size={20} />
@@ -134,7 +124,7 @@ state.worldEditor.refreshObjectTypeCaches(id);
<div style={{ flex: 1, position: 'relative' }}>
<Canvas camera={{ position: [12, 10, 12], fov: 50 }}>
<VoxelEditorScene
voxels={objectType.voxels.slice()}
voxels={objectType.objectType.voxels.slice()}
mode={mode}
color={color}
typeId="stone"
@@ -3,7 +3,7 @@ import { useState, useMemo, useEffect } from 'react';
import type { ThreeEvent } from '@react-three/fiber';
import type { Ray } from 'three';
import type { Voxel, V3 } from '../../types';
import { buildVoxelMesh } from '../../utils/graphics/voxelMesh';
import { buildVoxelGroupMesh } from '../../utils/graphics/voxelMesh';
type Props = {
voxels: Voxel[];
@@ -64,7 +64,7 @@ interface MergedVoxelMeshProps {
function MergedVoxelMesh({ color, positions, onPointerMove, onClick, onPointerLeave }: MergedVoxelMeshProps) {
const geo = useMemo(
() => buildVoxelMesh(positions),
() => buildVoxelGroupMesh(positions),
[positions],
);
useEffect(() => () => { geo.dispose(); }, [geo]);