186 lines
5.8 KiB
JavaScript
186 lines
5.8 KiB
JavaScript
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 don’t 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
|
||
}
|