Files
bo3-js/js/instances/RenderService.js
usernames122 88d231e367 init commit
2025-10-12 18:03:20 +02:00

186 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { Instance } from "./Instance.js";
import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js";
import * as CANNON from "cannon-es";
export class RenderService extends Instance {
constructor() {
super("RenderService");
// Add canvas to DOM
if (typeof document !== "undefined") {
// Three.js setup
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
// Controls and loader
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.loader = new GLTFLoader();
}
// Handle window resize
if (typeof window !== "undefined") {
window.addEventListener("resize", () => this.updateWindowSize());
// Basic lighting
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
this.scene.add(light);
// Default test cube (you can remove this later)
/*const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
this.scene.add(cube);*/
// I commented out the cube because it was annoying me
// just like the blender default cube
// Camera position
this.camera.position.z = 5;
}
// Rendering state
this._running = false;
this._lastFrame = 0;
this.isServer = false; // If this is true it will ignore rendering and only run physics. Useful for dedicated servers.
// Event signals
this.RenderStepped = new BO3ScriptSignal();
this.Heartbeat = new BO3ScriptSignal();
this.Stepped = new BO3ScriptSignal();
// Physics world
this.physicsWorld = null;
this._initPhysicsWorld();
// Debug text
this.debugText = ["Debug text!"]; // Line array
// Debug overlay
if (typeof document !== "undefined") {
this.overlay = document.createElement("canvas");
this.overlay.style.position = "absolute";
this.overlay.style.top = "0";
this.overlay.style.left = "0";
this.overlay.style.pointerEvents = "none";
this.overlay.width = window.innerWidth;
this.overlay.height = window.innerHeight;
document.body.appendChild(this.overlay);
this.debugCtx = this.overlay.getContext("2d");
}
}
// === Instance Overrides ===
Destroy() {
if (typeof window !== "undefined") {
window.removeEventListener("resize", this.updateWindowSize);
}
if (this.renderer.domElement?.parentNode) {
this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
}
}
// === Rendering ===
updateWindowSize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
if (this.overlay) {
this.overlay.width = window.innerWidth;
this.overlay.height = window.innerHeight;
}
}
// === Main loop ===
start() {
if (this._running) return;
this._running = true;
const loop = (time) => {
if (!this._running) return;
const dt = (time - this._lastFrame) / 1000;
this._lastFrame = time;
this.renderFrame(dt);
if (typeof window !== "undefined") requestAnimationFrame(loop);
};
if (typeof window !== "undefined") {
requestAnimationFrame(loop);
} else {
// Server-side loop at ~60Hz
setInterval(() => {
const now = Date.now();
const dt = (now - this._lastFrame) / 1000;
this._lastFrame = now;
this.renderFrame(dt);
}, 1000 / 60);
}
}
stop() {
this._running = false;
}
_initPhysicsWorld() {
this.physicsWorld = new CANNON.World();
this.physicsWorld.gravity.set(0, -9.81, 0);
}
renderFrame(dt) {
// Emit Stepped signals
this.Stepped.Fire(dt);
if (this.physicsWorld) {
this.physicsWorld.step(1 / 60, dt, 3);
}
if (!this.isServer) {
// update all BaseParts visuals
this.scene.traverse(obj => {
const base = obj.userData?.basePart;
if (base) {
base.updateVisual();
//base.clientUpdate(dt);
//console.debug("Updated visual for BasePart:", base.Name);
} else if (obj.isMesh && !obj.userData?.checked) {
obj.userData.checked = true; // mark it so we dont warn every frame
console.debug("[RenderService] Non-BasePart mesh (ignored):", obj.name || obj);
}
});
this.RenderStepped.Fire(dt);
// Render debug text
if (this.debugCtx && this.debugText.length > 0) {
const ctx = this.debugCtx;
ctx.clearRect(0, 0, this.overlay.width, this.overlay.height);
ctx.save();
ctx.font = "14px Arial";
ctx.fillStyle = "white";
ctx.textAlign = "left";
ctx.textBaseline = "top";
this.debugText.forEach((line, i) => {
ctx.fillText(line, 10, 10 + i * 16);
});
ctx.restore();
}
this.controls.update();
this.renderer.render(this.scene, this.camera);
} else {
// Update position variables for all BaseParts from physicsWorld, scene doesnt exist
this.physicsWorld.bodies.forEach(body => {
const base = body.userData?.basePart;
if (base) {
base.OnPhysicsTick();
}
});
}
this.Heartbeat.Fire(dt);
} // Just like roblox, Heartbeat is always fired, RenderStepped only on clients, Stepped always
}