From a60e01e4d152b0d75b7ebddd47b88cb92de7d960 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Sun, 21 Jun 2026 15:38:41 +0300 Subject: [PATCH] layered geometries --- webclient/src/App.scss | 5 + webclient/src/App.tsx | 4 +- webclient/src/components/Panels.scss | 2 +- webclient/src/components/SceneEditorView.tsx | 28 +---- .../src/components/layers/GameraLayer.tsx | 27 ----- .../components/layers/LayeredThreeView.tsx | 20 ++-- .../src/components/layers/LighsLayer.tsx | 8 ++ .../layers/ObjectGeometriesLayer.tsx | 5 +- .../layers/SceneEditorGameraLayer.tsx | 41 +++++++ .../src/components/layers/StatsLayer.tsx | 71 ++++++++++++ webclient/src/components/tools/CameraSync.tsx | 16 --- webclient/src/state/viewLayers/base.ts | 2 + webclient/src/state/viewLayers/camera.ts | 26 +++++ webclient/src/state/viewLayers/index.ts | 4 + .../src/state/viewLayers/objectGeometry.ts | 16 +++ .../state/viewLayers/objectTypeGeometry.ts | 81 +++++++++++++ webclient/src/state/viewState.ts | 107 ++---------------- webclient/src/state/worldEditorState.ts | 2 + webclient/src/state/worldState.ts | 4 +- webclient/src/types/model/world.ts | 6 +- webclient/src/utils/hooks/useLayerFrame.tsx | 16 +++ webclient/src/utils/hooks/useSyncCamera.tsx | 17 +++ webclient/src/utils/layerTimings.ts | 1 + 23 files changed, 322 insertions(+), 187 deletions(-) delete mode 100644 webclient/src/components/layers/GameraLayer.tsx create mode 100644 webclient/src/components/layers/LighsLayer.tsx create mode 100644 webclient/src/components/layers/SceneEditorGameraLayer.tsx create mode 100644 webclient/src/components/layers/StatsLayer.tsx delete mode 100644 webclient/src/components/tools/CameraSync.tsx create mode 100644 webclient/src/state/viewLayers/base.ts create mode 100644 webclient/src/state/viewLayers/camera.ts create mode 100644 webclient/src/state/viewLayers/index.ts create mode 100644 webclient/src/state/viewLayers/objectGeometry.ts create mode 100644 webclient/src/state/viewLayers/objectTypeGeometry.ts create mode 100644 webclient/src/utils/hooks/useLayerFrame.tsx create mode 100644 webclient/src/utils/hooks/useSyncCamera.tsx create mode 100644 webclient/src/utils/layerTimings.ts diff --git a/webclient/src/App.scss b/webclient/src/App.scss index f90339d..0e5f399 100644 --- a/webclient/src/App.scss +++ b/webclient/src/App.scss @@ -1,3 +1,8 @@ +.editor-layout { + width: 100vw; + height: 100vh; +} + .counter { font-size: 16px; padding: 5px 10px; diff --git a/webclient/src/App.tsx b/webclient/src/App.tsx index 46bfece..5f9c0ae 100644 --- a/webclient/src/App.tsx +++ b/webclient/src/App.tsx @@ -92,11 +92,11 @@ function HomePage() { function EditorLayout() { return ( - <> +
- +
); } diff --git a/webclient/src/components/Panels.scss b/webclient/src/components/Panels.scss index 6770fdb..227441b 100644 --- a/webclient/src/components/Panels.scss +++ b/webclient/src/components/Panels.scss @@ -1,5 +1,5 @@ .overlay-panels { - z-index: 15000; + z-index: 9000; position: fixed; top: 0; left: 0; diff --git a/webclient/src/components/SceneEditorView.tsx b/webclient/src/components/SceneEditorView.tsx index aee90a1..16443a4 100644 --- a/webclient/src/components/SceneEditorView.tsx +++ b/webclient/src/components/SceneEditorView.tsx @@ -5,48 +5,31 @@ import { observer } from 'mobx-react-lite'; import { state } from '../state'; import { SceneView } from './SceneView'; 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'; export const SceneEditorView = observer(function () { const controlsRef = useRef>(null); - const cameraAtStart = useRef(''); - - 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); - }; + useSyncCamera(() => state.view.camera.camera); const handleEnd = () => { const controls = controlsRef.current; if (!controls) return; - const snapshot = cameraSnapshot(controls.object); - if (snapshot === cameraAtStart.current) - return; + const [x, y, z] = controls.object.rotation.toArray(); - state.worldEditor.setCamera({ + state.view.camera.camera = { position: controls.object.position.toArray(), look: [x, y, z], - }); + }; }; return (<> - {/* { - // 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; -} diff --git a/webclient/src/components/layers/LayeredThreeView.tsx b/webclient/src/components/layers/LayeredThreeView.tsx index dab7208..6f215d8 100644 --- a/webclient/src/components/layers/LayeredThreeView.tsx +++ b/webclient/src/components/layers/LayeredThreeView.tsx @@ -1,23 +1,19 @@ import { observer } from "mobx-react-lite"; import { ObjectGeometriesLayer } from "./ObjectGeometriesLayer"; import { Canvas } from "@react-three/fiber"; -import { Perf } from "r3f-perf"; -import { state } from "../../state"; -import { CameraLayer } from "./GameraLayer"; +import { SceneEditorCameraLayer } from "./SceneEditorGameraLayer"; +import { LightsLayer } from "./LighsLayer"; +import { StatsLayer } from './StatsLayer'; export const LayeredThreeView = observer(function () { return <> - - - - - - - + + + {/* */} + + ; }); diff --git a/webclient/src/components/layers/LighsLayer.tsx b/webclient/src/components/layers/LighsLayer.tsx new file mode 100644 index 0000000..831d472 --- /dev/null +++ b/webclient/src/components/layers/LighsLayer.tsx @@ -0,0 +1,8 @@ +import { observer } from "mobx-react-lite"; + +export const LightsLayer = observer(function () { + return <> + + + ; +}); diff --git a/webclient/src/components/layers/ObjectGeometriesLayer.tsx b/webclient/src/components/layers/ObjectGeometriesLayer.tsx index 95e9723..af08fed 100644 --- a/webclient/src/components/layers/ObjectGeometriesLayer.tsx +++ b/webclient/src/components/layers/ObjectGeometriesLayer.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef } from "react"; -import { useThree, useFrame } from "@react-three/fiber"; +import { useThree } from "@react-three/fiber"; import { ObjectGeometryList } from "../../rendering/ObjectGeometryList"; import { observer } from "mobx-react-lite"; import { state } from "../../state"; +import { useLayerFrame } from "../../utils/hooks/useLayerFrame"; export const ObjectGeometriesLayer = observer(function () { @@ -20,7 +21,7 @@ export const ObjectGeometriesLayer = observer(function () { }; }, [threeScene, geometries]); - useFrame(() => list.current?.tick()); + useLayerFrame("ObjectGeometries", () => list.current?.tick()); return null; }); diff --git a/webclient/src/components/layers/SceneEditorGameraLayer.tsx b/webclient/src/components/layers/SceneEditorGameraLayer.tsx new file mode 100644 index 0000000..141d3ca --- /dev/null +++ b/webclient/src/components/layers/SceneEditorGameraLayer.tsx @@ -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>(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 ; +}); diff --git a/webclient/src/components/layers/StatsLayer.tsx b/webclient/src/components/layers/StatsLayer.tsx new file mode 100644 index 0000000..2615c24 --- /dev/null +++ b/webclient/src/components/layers/StatsLayer.tsx @@ -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(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; +}; diff --git a/webclient/src/components/tools/CameraSync.tsx b/webclient/src/components/tools/CameraSync.tsx deleted file mode 100644 index 70df28a..0000000 --- a/webclient/src/components/tools/CameraSync.tsx +++ /dev/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; -}); \ No newline at end of file diff --git a/webclient/src/state/viewLayers/base.ts b/webclient/src/state/viewLayers/base.ts new file mode 100644 index 0000000..9706ea0 --- /dev/null +++ b/webclient/src/state/viewLayers/base.ts @@ -0,0 +1,2 @@ +export abstract class ViewItem { +} diff --git a/webclient/src/state/viewLayers/camera.ts b/webclient/src/state/viewLayers/camera.ts new file mode 100644 index 0000000..f281820 --- /dev/null +++ b/webclient/src/state/viewLayers/camera.ts @@ -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 { +} diff --git a/webclient/src/state/viewLayers/index.ts b/webclient/src/state/viewLayers/index.ts new file mode 100644 index 0000000..463aeb7 --- /dev/null +++ b/webclient/src/state/viewLayers/index.ts @@ -0,0 +1,4 @@ +export * from './base'; +export * from './objectTypeGeometry'; +export * from './objectGeometry'; +export * from './camera'; diff --git a/webclient/src/state/viewLayers/objectGeometry.ts b/webclient/src/state/viewLayers/objectGeometry.ts new file mode 100644 index 0000000..8f5a8e9 --- /dev/null +++ b/webclient/src/state/viewLayers/objectGeometry.ts @@ -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; + } +} diff --git a/webclient/src/state/viewLayers/objectTypeGeometry.ts b/webclient/src/state/viewLayers/objectTypeGeometry.ts new file mode 100644 index 0000000..1f0d5fd --- /dev/null +++ b/webclient/src/state/viewLayers/objectTypeGeometry.ts @@ -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); + } +} diff --git a/webclient/src/state/viewState.ts b/webclient/src/state/viewState.ts index 4b886cd..203e714 100644 --- a/webclient/src/state/viewState.ts +++ b/webclient/src/state/viewState.ts @@ -1,109 +1,21 @@ -import * as THREE from 'three'; -import { action, makeAutoObservable, makeObservable, observable, reaction, type IReactionDisposer } from "mobx"; -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 { makeAutoObservable } from "mobx"; +import type { ObjectInstance, ObjectType } from "../types"; import { MobxReconcilerMap } from '../utils/reconciler'; - -export abstract class ViewItem { -} - -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; - } -} +import { ObjectGeometryViewItem, ObjectTypeGeometryViewItem, ControlledCameraViewItem, ViewItem } from './viewLayers'; +import type { WorldState } from "./worldState"; export class ViewState { public objectTypeGeometries: MobxReconcilerMap; public objectGeometries: MobxReconcilerMap; + public camera?: ControlledCameraViewItem; - constructor(world: { data: { objectTypes: Record; initialScene: Scene } }) { + constructor(world: WorldState) { makeAutoObservable( this, ); + + this.camera = new ControlledCameraViewItem(); + this.objectTypeGeometries = new MobxReconcilerMap({ reaction: () => Object.values(world.data.objectTypes), keyOf: (ot) => ot.id, @@ -130,6 +42,7 @@ export class ViewState { return [ ...this.objectTypeGeometries, ...this.objectGeometries, + ...[this.camera ? [this.camera] : []], ]; } } diff --git a/webclient/src/state/worldEditorState.ts b/webclient/src/state/worldEditorState.ts index c64fae5..7af0be5 100644 --- a/webclient/src/state/worldEditorState.ts +++ b/webclient/src/state/worldEditorState.ts @@ -66,6 +66,8 @@ export class WorldEditorState { } public setCamera(value: Pos3): void { + if (JSON.stringify(value) === JSON.stringify(this.world.data.editorCamera)) + return; this.world.data.editorCamera = value; } diff --git a/webclient/src/state/worldState.ts b/webclient/src/state/worldState.ts index ad9bb4e..4a87294 100644 --- a/webclient/src/state/worldState.ts +++ b/webclient/src/state/worldState.ts @@ -1,6 +1,6 @@ import { autorun, makeAutoObservable, toJS } from "mobx"; 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 { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes"; import { wolf } from "../model/objectPrefabs/wolf"; @@ -8,7 +8,7 @@ import { terrainXZ } from "../model/objectPrefabs/terrain"; import { populateRuntimeWorld } from "../utils/runtime"; export class WorldState { - public data: RuntimeWorld = WorldFactory.create(); + public data: World = WorldFactory.create(); private _buildSave(): World { const d = this.data; diff --git a/webclient/src/types/model/world.ts b/webclient/src/types/model/world.ts index 4e187ac..89a88cd 100644 --- a/webclient/src/types/model/world.ts +++ b/webclient/src/types/model/world.ts @@ -1,6 +1,6 @@ import type { GameRules } from "./gameRules"; import type { ObjectType } from "./object"; -import type { RuntimeScene, Scene } from "./scene"; +import type { Scene } from "./scene"; import type { Pos3 } from "../3d"; import type { VoxelType } from "./voxel"; @@ -11,7 +11,3 @@ export type World = { initialScene: Scene; gameRules: GameRules; } - -export type RuntimeWorld = World & { - initialScene: RuntimeScene; -} diff --git a/webclient/src/utils/hooks/useLayerFrame.tsx b/webclient/src/utils/hooks/useLayerFrame.tsx new file mode 100644 index 0000000..dd6b3fa --- /dev/null +++ b/webclient/src/utils/hooks/useLayerFrame.tsx @@ -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); +} diff --git a/webclient/src/utils/hooks/useSyncCamera.tsx b/webclient/src/utils/hooks/useSyncCamera.tsx new file mode 100644 index 0000000..de37d8e --- /dev/null +++ b/webclient/src/utils/hooks/useSyncCamera.tsx @@ -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]); +} diff --git a/webclient/src/utils/layerTimings.ts b/webclient/src/utils/layerTimings.ts new file mode 100644 index 0000000..db99ecb --- /dev/null +++ b/webclient/src/utils/layerTimings.ts @@ -0,0 +1 @@ +export const layerTimings: Record = {};