Files
bo3-js/js/instances/RenderService.js

186 lines
5.8 KiB
JavaScript
Raw Normal View History

2025-10-12 18:03:20 +02:00
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
}