layered three view
game object geometries layer
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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(); };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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,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]);
|
||||
|
||||
Reference in New Issue
Block a user