layered geometries
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
.editor-layout {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.counter {
|
.counter {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ function HomePage() {
|
|||||||
|
|
||||||
function EditorLayout() {
|
function EditorLayout() {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="editor-layout">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<LayeredThreeView />
|
<LayeredThreeView />
|
||||||
<Panels />
|
<Panels />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.overlay-panels {
|
.overlay-panels {
|
||||||
z-index: 15000;
|
z-index: 9000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@@ -5,48 +5,31 @@ import { observer } from 'mobx-react-lite';
|
|||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
import { SceneView } from './SceneView';
|
import { SceneView } from './SceneView';
|
||||||
import { MOUSE, type OrthographicCamera, type PerspectiveCamera } from 'three';
|
import { MOUSE, type OrthographicCamera, type PerspectiveCamera } from 'three';
|
||||||
import { CameraSync } from './tools/CameraSync';
|
import { useSyncCamera } from '../utils/hooks/useSyncCamera';
|
||||||
import { Physics } from '@react-three/rapier';
|
import { Physics } from '@react-three/rapier';
|
||||||
|
|
||||||
|
|
||||||
export const SceneEditorView = observer(function () {
|
export const SceneEditorView = observer(function () {
|
||||||
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
||||||
|
|
||||||
const cameraAtStart = useRef<string>('');
|
useSyncCamera(() => state.view.camera.camera);
|
||||||
|
|
||||||
function cameraSnapshot(camera: OrthographicCamera | PerspectiveCamera): string {
|
|
||||||
return JSON.stringify({
|
|
||||||
position: camera.position,
|
|
||||||
rotation: camera.rotation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStart = () => {
|
|
||||||
const controls = controlsRef.current;
|
|
||||||
if (!controls)
|
|
||||||
return;
|
|
||||||
cameraAtStart.current = cameraSnapshot(controls.object);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnd = () => {
|
const handleEnd = () => {
|
||||||
const controls = controlsRef.current;
|
const controls = controlsRef.current;
|
||||||
if (!controls)
|
if (!controls)
|
||||||
return;
|
return;
|
||||||
const snapshot = cameraSnapshot(controls.object);
|
|
||||||
if (snapshot === cameraAtStart.current)
|
|
||||||
return;
|
|
||||||
const [x, y, z] = controls.object.rotation.toArray();
|
const [x, y, z] = controls.object.rotation.toArray();
|
||||||
state.worldEditor.setCamera({
|
state.view.camera.camera = {
|
||||||
position: controls.object.position.toArray(),
|
position: controls.object.position.toArray(),
|
||||||
look: [x, y, z],
|
look: [x, y, z],
|
||||||
});
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
ref={controlsRef}
|
ref={controlsRef}
|
||||||
onStart={handleStart}
|
|
||||||
onEnd={handleEnd}
|
onEnd={handleEnd}
|
||||||
makeDefault
|
makeDefault
|
||||||
mouseButtons={{
|
mouseButtons={{
|
||||||
@@ -57,7 +40,6 @@ export const SceneEditorView = observer(function () {
|
|||||||
rotateSpeed={0.2}
|
rotateSpeed={0.2}
|
||||||
enableDamping={false}
|
enableDamping={false}
|
||||||
/>
|
/>
|
||||||
<CameraSync camera={state.worldEditor.camera} />
|
|
||||||
{/* <Grid
|
{/* <Grid
|
||||||
sectionThickness={0.5}
|
sectionThickness={0.5}
|
||||||
cellThickness={0}
|
cellThickness={0}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,19 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ObjectGeometriesLayer } from "./ObjectGeometriesLayer";
|
import { ObjectGeometriesLayer } from "./ObjectGeometriesLayer";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { Perf } from "r3f-perf";
|
import { SceneEditorCameraLayer } from "./SceneEditorGameraLayer";
|
||||||
import { state } from "../../state";
|
import { LightsLayer } from "./LighsLayer";
|
||||||
import { CameraLayer } from "./GameraLayer";
|
import { StatsLayer } from './StatsLayer';
|
||||||
|
|
||||||
export const LayeredThreeView = observer(function () {
|
export const LayeredThreeView = observer(function () {
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Canvas
|
<Canvas>
|
||||||
// camera={state.world.character.camera}
|
<LightsLayer />
|
||||||
>
|
{/* <Perf position="bottom-right" /> */}
|
||||||
<ambientLight intensity={0.5} />
|
<SceneEditorCameraLayer />
|
||||||
<directionalLight position={[5, 5, 5]} intensity={1} />
|
|
||||||
|
|
||||||
<Perf position="bottom-right" />
|
|
||||||
|
|
||||||
<CameraLayer camera={state.worldEditor.camera} />
|
|
||||||
<ObjectGeometriesLayer />
|
<ObjectGeometriesLayer />
|
||||||
|
<StatsLayer />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</>;
|
</>;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
export const LightsLayer = observer(function () {
|
||||||
|
return <>
|
||||||
|
<ambientLight intensity={0.5} />
|
||||||
|
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||||
|
</>;
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useThree, useFrame } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import { ObjectGeometryList } from "../../rendering/ObjectGeometryList";
|
import { ObjectGeometryList } from "../../rendering/ObjectGeometryList";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { state } from "../../state";
|
import { state } from "../../state";
|
||||||
|
import { useLayerFrame } from "../../utils/hooks/useLayerFrame";
|
||||||
|
|
||||||
export const ObjectGeometriesLayer = observer(function () {
|
export const ObjectGeometriesLayer = observer(function () {
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ export const ObjectGeometriesLayer = observer(function () {
|
|||||||
};
|
};
|
||||||
}, [threeScene, geometries]);
|
}, [threeScene, geometries]);
|
||||||
|
|
||||||
useFrame(() => list.current?.tick());
|
useLayerFrame("ObjectGeometries", () => list.current?.tick());
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { OrbitControls } from "@react-three/drei";
|
||||||
|
import { state } from "../../state";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { useSyncCamera } from "../../utils/hooks/useSyncCamera";
|
||||||
|
import { MOUSE } from "three";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import type { V3 } from "../../types";
|
||||||
|
|
||||||
|
export const SceneEditorCameraLayer = observer(function () {
|
||||||
|
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
||||||
|
|
||||||
|
useSyncCamera(() => state.view.camera?.camera);
|
||||||
|
|
||||||
|
const handleEnd = () => {
|
||||||
|
const controls = controlsRef.current;
|
||||||
|
const camera = state.view.camera;
|
||||||
|
if (!controls || !camera)
|
||||||
|
return;
|
||||||
|
|
||||||
|
camera.camera = {
|
||||||
|
position: controls.object.position.toArray(),
|
||||||
|
look: controls.object.rotation.toArray().slice(0, 3) as V3,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!state.view.camera)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return <OrbitControls
|
||||||
|
ref={controlsRef}
|
||||||
|
onEnd={handleEnd}
|
||||||
|
makeDefault
|
||||||
|
mouseButtons={{
|
||||||
|
LEFT: MOUSE.ROTATE,
|
||||||
|
MIDDLE: MOUSE.PAN,
|
||||||
|
RIGHT: MOUSE.PAN,
|
||||||
|
}}
|
||||||
|
rotateSpeed={0.2}
|
||||||
|
enableDamping={false}
|
||||||
|
/>;
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import { layerTimings } from "../../utils/layerTimings";
|
||||||
|
|
||||||
|
export const StatsLayer = function () {
|
||||||
|
const divRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const fpsFrames = useRef(0);
|
||||||
|
const fpsLastTime = useRef(0);
|
||||||
|
const renderAcc = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fpsLastTime.current = performance.now();
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
Object.assign(div.style, {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "8px",
|
||||||
|
right: "8px",
|
||||||
|
color: "#0f0",
|
||||||
|
fontSize: "14px",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
zIndex: "9999",
|
||||||
|
pointerEvents: "none",
|
||||||
|
whiteSpace: "pre",
|
||||||
|
});
|
||||||
|
document.body.appendChild(div);
|
||||||
|
divRef.current = div;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(div);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame(({ gl, scene, camera }) => {
|
||||||
|
const renderStart = performance.now();
|
||||||
|
gl.render(scene, camera);
|
||||||
|
renderAcc.current += performance.now() - renderStart;
|
||||||
|
|
||||||
|
fpsFrames.current++;
|
||||||
|
const now = performance.now();
|
||||||
|
const elapsed = now - fpsLastTime.current;
|
||||||
|
if (elapsed >= 1000) {
|
||||||
|
const frames = fpsFrames.current;
|
||||||
|
const fps = Math.round(frames * 1000 / elapsed);
|
||||||
|
const renderAvg = (renderAcc.current / frames).toFixed(3);
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
`${fps} FPS gl.render ${renderAvg} ms`,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [name, entry] of Object.entries(layerTimings)) {
|
||||||
|
if (entry.frames > 0) {
|
||||||
|
const avg = (entry.acc / entry.frames).toFixed(3);
|
||||||
|
lines.push(`${name} ${avg} ms`);
|
||||||
|
}
|
||||||
|
entry.acc = 0;
|
||||||
|
entry.frames = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (divRef.current) {
|
||||||
|
divRef.current.textContent = lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsFrames.current = 0;
|
||||||
|
renderAcc.current = 0;
|
||||||
|
fpsLastTime.current = now;
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { useThree } from "@react-three/fiber";
|
|
||||||
import type { Pos3 } from "../../types";
|
|
||||||
import { useLayoutEffect } from "react";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
export const CameraSync = observer(function ({ camera }: { camera: Pos3 }) {
|
|
||||||
const { camera: threeCamera } = useThree();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
threeCamera.position.set(camera.position[0], camera.position[1], camera.position[2]);
|
|
||||||
threeCamera.rotation.set(camera.look[0], camera.look[1], camera.look[2]);
|
|
||||||
threeCamera.updateProjectionMatrix();
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export abstract class ViewItem {
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Pos3 } from "../../types";
|
||||||
|
import { state } from "../rootState";
|
||||||
|
import { ViewItem } from "./base";
|
||||||
|
|
||||||
|
export abstract class CameraViewItem extends ViewItem {
|
||||||
|
public abstract get camera(): Pos3;
|
||||||
|
public abstract set camera(value: Pos3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ControlledCameraViewItem extends CameraViewItem {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get camera(): Pos3 {
|
||||||
|
return state.worldEditor.camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set camera(value: Pos3) {
|
||||||
|
state.worldEditor.setCamera(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class ThirdPersonCameraViewItem extends CameraViewItem {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './base';
|
||||||
|
export * from './objectTypeGeometry';
|
||||||
|
export * from './objectGeometry';
|
||||||
|
export * from './camera';
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ObjectInstance, ObjectInstanceRigidBody } from "../../types";
|
||||||
|
import { ViewItem } from "./base";
|
||||||
|
import type { ObjectTypeGeometryViewItem } from './objectTypeGeometry';
|
||||||
|
|
||||||
|
export class ObjectGeometryViewItem extends ViewItem {
|
||||||
|
public readonly object: ObjectInstance;
|
||||||
|
public readonly objectTypeGeometry: ObjectTypeGeometryViewItem;
|
||||||
|
|
||||||
|
public rb: (() => ObjectInstanceRigidBody | null) | undefined;
|
||||||
|
|
||||||
|
constructor(object: ObjectInstance, objectTypeGeometry: ObjectTypeGeometryViewItem) {
|
||||||
|
super();
|
||||||
|
this.object = object;
|
||||||
|
this.objectTypeGeometry = objectTypeGeometry;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import type { ObjectType, V3, Voxel, VoxelGroup } from "../../types";
|
||||||
|
import { ViewItem } from "./base";
|
||||||
|
import { action, makeObservable, observable, reaction, type IReactionDisposer } from 'mobx';
|
||||||
|
import { getObjectVoxelGroups } from '../../utils/graphics/voxelGroup';
|
||||||
|
import { buildVoxelGroupMesh } from '../../utils/graphics/voxelMesh';
|
||||||
|
import { state } from '../rootState';
|
||||||
|
import { buildObjectTrimesh } from '../../utils/graphics/mesh';
|
||||||
|
import { computeBoundingBox } from '../../utils/runtime';
|
||||||
|
|
||||||
|
export class ObjectTypeGeometryViewItem extends ViewItem {
|
||||||
|
public readonly objectType: ObjectType;
|
||||||
|
|
||||||
|
public voxelGroups: VoxelGroup[] | undefined;
|
||||||
|
public colliderMesh: [Float32Array, Uint32Array] | undefined;
|
||||||
|
public boundingBox: { min: V3; max: V3 } | undefined;
|
||||||
|
public meshes: THREE.InstancedMesh[] | undefined;
|
||||||
|
|
||||||
|
private readonly _disposeVoxelReaction: IReactionDisposer;
|
||||||
|
|
||||||
|
constructor(objectType: ObjectType) {
|
||||||
|
super();
|
||||||
|
this.objectType = objectType;
|
||||||
|
makeObservable(this, {
|
||||||
|
voxelGroups: observable.ref,
|
||||||
|
colliderMesh: observable.ref,
|
||||||
|
boundingBox: observable.ref,
|
||||||
|
meshes: observable.ref,
|
||||||
|
updateRuntimeData: action,
|
||||||
|
});
|
||||||
|
this._disposeVoxelReaction = reaction(
|
||||||
|
() => objectType.voxels.map(v => `${v.position}-${v.typeId}`).join('|'),
|
||||||
|
() => this.updateRuntimeData(),
|
||||||
|
);
|
||||||
|
this.updateRuntimeData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this._disposeVoxelReaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public addVoxel(voxel: Voxel): boolean {
|
||||||
|
const occupied = this.objectType.voxels.some(v =>
|
||||||
|
v.position[0] === voxel.position[0] &&
|
||||||
|
v.position[1] === voxel.position[1] &&
|
||||||
|
v.position[2] === voxel.position[2]
|
||||||
|
);
|
||||||
|
if (!occupied) {
|
||||||
|
this.objectType.voxels.push(voxel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeVoxel(position: V3): Voxel | undefined {
|
||||||
|
const idx = this.objectType.voxels.findIndex(v =>
|
||||||
|
v.position[0] === position[0] &&
|
||||||
|
v.position[1] === position[1] &&
|
||||||
|
v.position[2] === position[2]
|
||||||
|
);
|
||||||
|
if (idx !== -1)
|
||||||
|
return this.objectType.voxels.splice(idx, 1)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateRuntimeData(): void {
|
||||||
|
this.voxelGroups = getObjectVoxelGroups(this.objectType, state.world.data.voxelTypes);
|
||||||
|
this.meshes = [];
|
||||||
|
for (const vg of this.voxelGroups) {
|
||||||
|
const geo = buildVoxelGroupMesh(vg.positions);
|
||||||
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
|
color: vg.color,
|
||||||
|
opacity: vg.opacity,
|
||||||
|
transparent: vg.opacity < 1,
|
||||||
|
});
|
||||||
|
const mesh = new THREE.InstancedMesh(geo, mat, 1000);
|
||||||
|
this.meshes.push(mesh);
|
||||||
|
}
|
||||||
|
this.colliderMesh = buildObjectTrimesh(this.objectType, state.world.data.voxelTypes);
|
||||||
|
this.boundingBox = computeBoundingBox(this.objectType);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,109 +1,21 @@
|
|||||||
import * as THREE from 'three';
|
import { makeAutoObservable } from "mobx";
|
||||||
import { action, makeAutoObservable, makeObservable, observable, reaction, type IReactionDisposer } from "mobx";
|
import type { ObjectInstance, ObjectType } from "../types";
|
||||||
import type { ObjectInstance, ObjectInstanceRigidBody, ObjectType, Scene, V3, Voxel, VoxelGroup } from "../types";
|
|
||||||
import { state } from "./rootState";
|
|
||||||
import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup";
|
|
||||||
import { buildVoxelGroupMesh } from "../utils/graphics/voxelMesh";
|
|
||||||
import { buildObjectTrimesh } from '../utils/graphics/mesh';
|
|
||||||
import { computeBoundingBox } from '../utils/runtime';
|
|
||||||
import { MobxReconcilerMap } from '../utils/reconciler';
|
import { MobxReconcilerMap } from '../utils/reconciler';
|
||||||
|
import { ObjectGeometryViewItem, ObjectTypeGeometryViewItem, ControlledCameraViewItem, ViewItem } from './viewLayers';
|
||||||
export abstract class ViewItem {
|
import type { WorldState } from "./worldState";
|
||||||
}
|
|
||||||
|
|
||||||
export class ObjectTypeGeometryViewItem extends ViewItem {
|
|
||||||
public readonly objectType: ObjectType;
|
|
||||||
|
|
||||||
public voxelGroups: VoxelGroup[] | undefined;
|
|
||||||
public colliderMesh: [Float32Array, Uint32Array] | undefined;
|
|
||||||
public boundingBox: { min: V3; max: V3 } | undefined;
|
|
||||||
public meshes: THREE.InstancedMesh[] | undefined;
|
|
||||||
|
|
||||||
private readonly _disposeVoxelReaction: IReactionDisposer;
|
|
||||||
|
|
||||||
constructor(objectType: ObjectType) {
|
|
||||||
super();
|
|
||||||
this.objectType = objectType;
|
|
||||||
makeObservable(this, {
|
|
||||||
voxelGroups: observable.ref,
|
|
||||||
colliderMesh: observable.ref,
|
|
||||||
boundingBox: observable.ref,
|
|
||||||
meshes: observable.ref,
|
|
||||||
updateRuntimeData: action,
|
|
||||||
});
|
|
||||||
this._disposeVoxelReaction = reaction(
|
|
||||||
() => objectType.voxels.map(v => `${v.position}-${v.typeId}`).join('|'),
|
|
||||||
() => this.updateRuntimeData(),
|
|
||||||
);
|
|
||||||
this.updateRuntimeData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
|
||||||
this._disposeVoxelReaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public addVoxel(voxel: Voxel): boolean {
|
|
||||||
const occupied = this.objectType.voxels.some(v =>
|
|
||||||
v.position[0] === voxel.position[0] &&
|
|
||||||
v.position[1] === voxel.position[1] &&
|
|
||||||
v.position[2] === voxel.position[2]
|
|
||||||
);
|
|
||||||
if (!occupied) {
|
|
||||||
this.objectType.voxels.push(voxel);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeVoxel(position: V3): Voxel | undefined {
|
|
||||||
const idx = this.objectType.voxels.findIndex(v =>
|
|
||||||
v.position[0] === position[0] &&
|
|
||||||
v.position[1] === position[1] &&
|
|
||||||
v.position[2] === position[2]
|
|
||||||
);
|
|
||||||
if (idx !== -1)
|
|
||||||
return this.objectType.voxels.splice(idx, 1)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateRuntimeData(): void {
|
|
||||||
this.voxelGroups = getObjectVoxelGroups(this.objectType, state.world.data.voxelTypes);
|
|
||||||
this.meshes = [];
|
|
||||||
for (const vg of this.voxelGroups) {
|
|
||||||
const geo = buildVoxelGroupMesh(vg.positions);
|
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
|
||||||
color: vg.color,
|
|
||||||
opacity: vg.opacity,
|
|
||||||
transparent: vg.opacity < 1,
|
|
||||||
});
|
|
||||||
const mesh = new THREE.InstancedMesh(geo, mat, 1000);
|
|
||||||
this.meshes.push(mesh);
|
|
||||||
}
|
|
||||||
this.colliderMesh = buildObjectTrimesh(this.objectType, state.world.data.voxelTypes);
|
|
||||||
this.boundingBox = computeBoundingBox(this.objectType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ObjectGeometryViewItem extends ViewItem {
|
|
||||||
public readonly object: ObjectInstance;
|
|
||||||
public readonly objectTypeGeometry: ObjectTypeGeometryViewItem;
|
|
||||||
|
|
||||||
public rb: (() => ObjectInstanceRigidBody | null) | undefined;
|
|
||||||
|
|
||||||
constructor(object: ObjectInstance, objectTypeGeometry: ObjectTypeGeometryViewItem) {
|
|
||||||
super();
|
|
||||||
this.object = object;
|
|
||||||
this.objectTypeGeometry = objectTypeGeometry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ViewState {
|
export class ViewState {
|
||||||
public objectTypeGeometries: MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>;
|
public objectTypeGeometries: MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>;
|
||||||
public objectGeometries: MobxReconcilerMap<ObjectInstance, ObjectGeometryViewItem>;
|
public objectGeometries: MobxReconcilerMap<ObjectInstance, ObjectGeometryViewItem>;
|
||||||
|
public camera?: ControlledCameraViewItem;
|
||||||
|
|
||||||
constructor(world: { data: { objectTypes: Record<string, ObjectType>; initialScene: Scene } }) {
|
constructor(world: WorldState) {
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.camera = new ControlledCameraViewItem();
|
||||||
|
|
||||||
this.objectTypeGeometries = new MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>({
|
this.objectTypeGeometries = new MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>({
|
||||||
reaction: () => Object.values(world.data.objectTypes),
|
reaction: () => Object.values(world.data.objectTypes),
|
||||||
keyOf: (ot) => ot.id,
|
keyOf: (ot) => ot.id,
|
||||||
@@ -130,6 +42,7 @@ export class ViewState {
|
|||||||
return [
|
return [
|
||||||
...this.objectTypeGeometries,
|
...this.objectTypeGeometries,
|
||||||
...this.objectGeometries,
|
...this.objectGeometries,
|
||||||
|
...[this.camera ? [this.camera] : []],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export class WorldEditorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setCamera(value: Pos3): void {
|
public setCamera(value: Pos3): void {
|
||||||
|
if (JSON.stringify(value) === JSON.stringify(this.world.data.editorCamera))
|
||||||
|
return;
|
||||||
this.world.data.editorCamera = value;
|
this.world.data.editorCamera = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { autorun, makeAutoObservable, toJS } from "mobx";
|
import { autorun, makeAutoObservable, toJS } from "mobx";
|
||||||
import { WorldFactory } from "../model/worldFactory";
|
import { WorldFactory } from "../model/worldFactory";
|
||||||
import { type ObjectInstance, type ObjectType, type World, type VoxelType, type RuntimeWorld } from "../types";
|
import { type ObjectInstance, type ObjectType, type World, type VoxelType } from "../types";
|
||||||
import { state } from "./rootState";
|
import { state } from "./rootState";
|
||||||
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
|
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
|
||||||
import { wolf } from "../model/objectPrefabs/wolf";
|
import { wolf } from "../model/objectPrefabs/wolf";
|
||||||
@@ -8,7 +8,7 @@ import { terrainXZ } from "../model/objectPrefabs/terrain";
|
|||||||
import { populateRuntimeWorld } from "../utils/runtime";
|
import { populateRuntimeWorld } from "../utils/runtime";
|
||||||
|
|
||||||
export class WorldState {
|
export class WorldState {
|
||||||
public data: RuntimeWorld = WorldFactory.create();
|
public data: World = WorldFactory.create();
|
||||||
|
|
||||||
private _buildSave(): World {
|
private _buildSave(): World {
|
||||||
const d = this.data;
|
const d = this.data;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { GameRules } from "./gameRules";
|
import type { GameRules } from "./gameRules";
|
||||||
import type { ObjectType } from "./object";
|
import type { ObjectType } from "./object";
|
||||||
import type { RuntimeScene, Scene } from "./scene";
|
import type { Scene } from "./scene";
|
||||||
import type { Pos3 } from "../3d";
|
import type { Pos3 } from "../3d";
|
||||||
import type { VoxelType } from "./voxel";
|
import type { VoxelType } from "./voxel";
|
||||||
|
|
||||||
@@ -11,7 +11,3 @@ export type World = {
|
|||||||
initialScene: Scene;
|
initialScene: Scene;
|
||||||
gameRules: GameRules;
|
gameRules: GameRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuntimeWorld = World & {
|
|
||||||
initialScene: RuntimeScene;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { useFrame, type RootState } from "@react-three/fiber";
|
||||||
|
import { layerTimings } from "../layerTimings";
|
||||||
|
|
||||||
|
export function useLayerFrame(
|
||||||
|
name: string,
|
||||||
|
callback: (state: RootState, delta: number) => void,
|
||||||
|
priority?: number,
|
||||||
|
) {
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
const start = performance.now();
|
||||||
|
callback(state, delta);
|
||||||
|
const entry = layerTimings[name] ??= { acc: 0, frames: 0 };
|
||||||
|
entry.acc += performance.now() - start;
|
||||||
|
entry.frames++;
|
||||||
|
}, priority);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import type { Pos3 } from "../../types";
|
||||||
|
import { useLayoutEffect } from "react";
|
||||||
|
import { autorun } from "mobx";
|
||||||
|
|
||||||
|
export function useSyncCamera(getCamera: () => Pos3 | undefined) {
|
||||||
|
const { camera: threeCamera } = useThree();
|
||||||
|
|
||||||
|
useLayoutEffect(() => autorun(() => {
|
||||||
|
const camera = getCamera();
|
||||||
|
if (camera) {
|
||||||
|
threeCamera.position.set(camera.position[0], camera.position[1], camera.position[2]);
|
||||||
|
threeCamera.rotation.set(camera.look[0], camera.look[1], camera.look[2]);
|
||||||
|
threeCamera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}), [getCamera, threeCamera]);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const layerTimings: Record<string, { acc: number; frames: number }> = {};
|
||||||
Reference in New Issue
Block a user