ui: move rendering logic into renderLoop singleton
This commit is contained in:
131
pkgs/clan-app/ui/src/scene/RenderLoop.ts
Normal file
131
pkgs/clan-app/ui/src/scene/RenderLoop.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Scene, Camera, WebGLRenderer } from "three";
|
||||
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
|
||||
import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js";
|
||||
|
||||
/**
|
||||
* Private class dont try to import it
|
||||
*
|
||||
*/
|
||||
class RenderLoop {
|
||||
// Track if a render is already requested
|
||||
// This prevents multiple requests in the same frame
|
||||
// and ensures only one render per frame
|
||||
// This is important for performance and to avoid flickering
|
||||
private renderRequested = false;
|
||||
|
||||
// References to the scene, camera, renderer, controls, and label renderer
|
||||
// These will be set during initialization
|
||||
private scene!: Scene;
|
||||
private bgScene!: Scene;
|
||||
private camera!: Camera;
|
||||
private bgCamera!: Camera;
|
||||
private renderer!: WebGLRenderer;
|
||||
private controls!: MapControls;
|
||||
private labelRenderer!: CSS2DRenderer;
|
||||
|
||||
// Flag to prevent multiple initializations
|
||||
private initialized = false;
|
||||
|
||||
init(
|
||||
scene: Scene,
|
||||
camera: Camera,
|
||||
renderer: WebGLRenderer,
|
||||
labelRenderer: CSS2DRenderer,
|
||||
controls: MapControls,
|
||||
bgScene: Scene,
|
||||
bgCamera: Camera,
|
||||
) {
|
||||
if (this.initialized) {
|
||||
console.error("RenderLoop already initialized.");
|
||||
return;
|
||||
}
|
||||
this.scene = scene;
|
||||
this.camera = camera;
|
||||
this.renderer = renderer;
|
||||
this.controls = controls;
|
||||
this.bgScene = bgScene;
|
||||
this.bgCamera = bgCamera;
|
||||
this.labelRenderer = labelRenderer;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
requestRender() {
|
||||
// If not initialized, log an error and return
|
||||
if (!this.initialized) {
|
||||
console.error(
|
||||
"RenderLoop not initialized yet. Make sure to call init() once before usage.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// If a render is already requested, do nothing
|
||||
if (this.renderRequested) return;
|
||||
|
||||
this.renderRequested = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.updateTweens();
|
||||
|
||||
const needsUpdate = this.controls.update(); // returns true if damping is ongoing
|
||||
|
||||
this.render();
|
||||
this.renderRequested = false;
|
||||
|
||||
if (needsUpdate) {
|
||||
this.requestRender();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateTweens() {
|
||||
// TODO: TWEEN.update() for tween animations in the future
|
||||
}
|
||||
|
||||
private render() {
|
||||
// TODO: Disable console.debug in production
|
||||
console.debug("Rendering scene...", this);
|
||||
|
||||
this.renderer.autoClear = false;
|
||||
this.renderer.clear();
|
||||
|
||||
this.renderer.render(this.bgScene, this.bgCamera);
|
||||
|
||||
this.controls.update();
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
|
||||
this.labelRenderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Dispose controls, renderer, remove listeners if any
|
||||
this.controls.dispose();
|
||||
this.renderer.dispose();
|
||||
// clear refs
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton instance of RenderLoop
|
||||
* This is used to manage the re-rendering
|
||||
*
|
||||
* It can only be initialized once then passed to individual components
|
||||
* they can use the renderLoop to request re-renders as needed.
|
||||
*
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { renderLoop } from "./RenderLoop";
|
||||
*
|
||||
* // Somewhere initialize the render loop:
|
||||
* renderLoop.init(scene, camera, renderer, labelRenderer, controls, bgScene, bgCamera);
|
||||
*
|
||||
* // To request a render:
|
||||
* renderLoop.requestRender();
|
||||
*
|
||||
* // To dispose:
|
||||
* onCleanup(() => {
|
||||
* renderLoop.dispose();
|
||||
* })
|
||||
*
|
||||
*/
|
||||
export const renderLoop = new RenderLoop();
|
||||
@@ -22,6 +22,7 @@ import { MachinesQueryResult } from "../queries/queries";
|
||||
import { SceneData } from "../stores/clan";
|
||||
import { unwrap } from "solid-js/store";
|
||||
import { Accessor } from "solid-js";
|
||||
import { renderLoop } from "./RenderLoop";
|
||||
|
||||
function garbageCollectGroup(group: THREE.Group) {
|
||||
for (const child of group.children) {
|
||||
@@ -88,8 +89,6 @@ export function CubeScene(props: {
|
||||
const raycaster = new THREE.Raycaster();
|
||||
let initBase: THREE.Mesh | undefined;
|
||||
|
||||
let needsRender = false; // Flag to control rendering
|
||||
|
||||
// Create background scene
|
||||
const bgScene = new THREE.Scene();
|
||||
const bgCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
@@ -101,13 +100,6 @@ export function CubeScene(props: {
|
||||
let sharedCubeGeometry: THREE.BoxGeometry;
|
||||
let sharedBaseGeometry: THREE.BoxGeometry;
|
||||
|
||||
// Used for development purposes
|
||||
// Vite does hot-reload but we need to ensure the animation loop doesn't run multiple times
|
||||
// This flag prevents multiple animation loops from running simultaneously
|
||||
// It is set to true when the component mounts and false when it unmounts
|
||||
let isAnimating = false; // Flag to prevent multiple loops
|
||||
let frameCount = 0;
|
||||
|
||||
const [positionMode, setPositionMode] = createSignal<"grid" | "circle">(
|
||||
"grid",
|
||||
);
|
||||
@@ -180,35 +172,6 @@ export function CubeScene(props: {
|
||||
}
|
||||
});
|
||||
|
||||
function requestRenderIfNotRequested() {
|
||||
if (!needsRender) {
|
||||
needsRender = true;
|
||||
requestAnimationFrame(renderScene);
|
||||
}
|
||||
}
|
||||
|
||||
function renderScene() {
|
||||
if (!isAnimating) {
|
||||
console.warn("Not animating!");
|
||||
return;
|
||||
}
|
||||
console.log("Rendering scene...", camera.toJSON());
|
||||
|
||||
needsRender = false;
|
||||
|
||||
frameCount++;
|
||||
|
||||
renderer.autoClear = false;
|
||||
renderer.render(bgScene, bgCamera);
|
||||
|
||||
controls.update(); // optional; see note below
|
||||
|
||||
renderer.render(scene, camera);
|
||||
labelRenderer.render(scene, camera);
|
||||
|
||||
if (frameCount % 30 === 0) logMemoryUsage();
|
||||
}
|
||||
|
||||
function getGridPosition(id: string): [number, number, number] {
|
||||
// TODO: Detect collision with other cubes
|
||||
const machine = props.sceneStore()[id];
|
||||
@@ -341,7 +304,7 @@ export function CubeScene(props: {
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
requestRenderIfNotRequested();
|
||||
renderLoop.requestRender();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,20 +394,7 @@ export function CubeScene(props: {
|
||||
cubeMaterial.color.set(CUBE_COLOR);
|
||||
}
|
||||
|
||||
requestRenderIfNotRequested();
|
||||
}
|
||||
|
||||
function logMemoryUsage() {
|
||||
if (renderer && renderer.info) {
|
||||
console.debug("Three.js Memory:", {
|
||||
frame: renderer.info.render.frame,
|
||||
calls: renderer.info.render.calls,
|
||||
geometries: renderer.info.memory.geometries,
|
||||
textures: renderer.info.memory.textures,
|
||||
programs: renderer.info.programs?.length || 0,
|
||||
triangles: renderer.info.render.triangles,
|
||||
});
|
||||
}
|
||||
renderLoop.requestRender();
|
||||
}
|
||||
|
||||
const initialCameraPosition = { x: 20, y: 20, z: 20 };
|
||||
@@ -548,17 +498,26 @@ export function CubeScene(props: {
|
||||
controls.minZoom = 1.2;
|
||||
controls.maxZoom = 3.5;
|
||||
controls.addEventListener("change", () => {
|
||||
const aspect = container.clientWidth / container.clientHeight;
|
||||
const zoom = camera.zoom;
|
||||
camera.left = (-d * aspect) / zoom;
|
||||
camera.right = (d * aspect) / zoom;
|
||||
camera.top = d / zoom;
|
||||
camera.bottom = -d / zoom;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
requestRenderIfNotRequested();
|
||||
// const aspect = container.clientWidth / container.clientHeight;
|
||||
// const zoom = camera.zoom;
|
||||
// camera.left = (-d * aspect) / zoom;
|
||||
// camera.right = (d * aspect) / zoom;
|
||||
// camera.top = d / zoom;
|
||||
// camera.bottom = -d / zoom;
|
||||
// camera.updateProjectionMatrix();
|
||||
renderLoop.requestRender();
|
||||
});
|
||||
|
||||
renderLoop.init(
|
||||
scene,
|
||||
camera,
|
||||
renderer,
|
||||
labelRenderer,
|
||||
controls,
|
||||
bgScene,
|
||||
bgCamera,
|
||||
);
|
||||
|
||||
// Lighting
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||
scene.add(ambientLight);
|
||||
@@ -667,7 +626,7 @@ export function CubeScene(props: {
|
||||
} else {
|
||||
initBase!.visible = false;
|
||||
}
|
||||
requestRenderIfNotRequested();
|
||||
renderLoop.requestRender();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -718,9 +677,7 @@ export function CubeScene(props: {
|
||||
|
||||
renderer.domElement.addEventListener("click", onClick);
|
||||
|
||||
isAnimating = true;
|
||||
|
||||
requestRenderIfNotRequested();
|
||||
renderLoop.requestRender();
|
||||
|
||||
// Handle window resize
|
||||
const handleResize = () => {
|
||||
@@ -742,7 +699,7 @@ export function CubeScene(props: {
|
||||
);
|
||||
|
||||
renderer.render(bgScene, bgCamera);
|
||||
requestRenderIfNotRequested();
|
||||
renderLoop.requestRender();
|
||||
};
|
||||
|
||||
renderer.domElement.addEventListener("mousemove", onMouseMove);
|
||||
@@ -760,8 +717,8 @@ export function CubeScene(props: {
|
||||
|
||||
// Cleanup function
|
||||
onCleanup(() => {
|
||||
// Stop animation loop
|
||||
isAnimating = false;
|
||||
renderLoop.dispose();
|
||||
|
||||
renderer.domElement.removeEventListener("click", onClick);
|
||||
renderer.domElement.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
@@ -886,7 +843,7 @@ export function CubeScene(props: {
|
||||
}
|
||||
});
|
||||
|
||||
requestRenderIfNotRequested();
|
||||
renderLoop.requestRender();
|
||||
});
|
||||
|
||||
createEffect(
|
||||
@@ -919,7 +876,7 @@ export function CubeScene(props: {
|
||||
initBase.position.set(pos[0], BASE_HEIGHT / 2, pos[1]);
|
||||
initBase.visible = true;
|
||||
}
|
||||
requestRenderIfNotRequested();
|
||||
renderLoop.requestRender();
|
||||
};
|
||||
|
||||
const onAddClick = (event: MouseEvent) => {
|
||||
@@ -955,7 +912,7 @@ export function CubeScene(props: {
|
||||
// Only request render if the position actually changed
|
||||
initBase.position.set(snapped.x, 0, snapped.z);
|
||||
setCursorPosition([snapped.x, snapped.z]); // Update next position for cube creation
|
||||
requestRenderIfNotRequested();
|
||||
renderLoop.requestRender();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user