phantasmal-world/src/core/rendering/ThreeRenderer.ts

143 lines
4.0 KiB
TypeScript
Raw Normal View History

2020-01-20 00:16:28 +08:00
import CameraControls from "camera-controls";
import * as THREE from "three";
import {
Clock,
Color,
Group,
HemisphereLight,
OrthographicCamera,
PerspectiveCamera,
Scene,
Vector2,
Vector3,
} from "three";
import { Disposable } from "../observable/Disposable";
import { Renderer } from "./Renderer";
CameraControls.install({
// Hack to make panning and orbiting work the way we want.
THREE: {
...THREE,
MOUSE: { ...THREE.MOUSE, LEFT: THREE.MOUSE.RIGHT, RIGHT: THREE.MOUSE.LEFT },
},
});
export interface DisposableThreeRenderer extends THREE.Renderer, Disposable {}
/**
* Uses THREE.js for rendering.
*/
export abstract class ThreeRenderer extends Renderer {
private _debug = false;
get debug(): boolean {
return this._debug;
}
set debug(debug: boolean) {
this._debug = debug;
}
abstract readonly camera: PerspectiveCamera | OrthographicCamera;
readonly controls!: CameraControls;
readonly scene = new Scene();
readonly light_holder = new Group();
private readonly renderer: DisposableThreeRenderer;
private render_scheduled = false;
private animation_frame_handle?: number = undefined;
private readonly light = new HemisphereLight(0xffffff, 0x505050, 1.0);
private readonly controls_clock = new Clock();
private readonly size = new Vector2(0, 0);
protected constructor(three_renderer: DisposableThreeRenderer) {
super();
this.renderer = three_renderer;
this.renderer.domElement.tabIndex = 0;
this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down);
this.renderer.domElement.style.outline = "none";
this.scene.background = new Color(0x181818);
this.light_holder.add(this.light);
this.scene.add(this.light_holder);
}
get canvas_element(): HTMLCanvasElement {
return this.renderer.domElement;
}
set_size(width: number, height: number): void {
this.size.set(width, height);
this.renderer.setSize(width, height);
this.schedule_render();
}
pointer_pos_to_device_coords(pos: Vector2): void {
pos.set((pos.x / this.size.width) * 2 - 1, (pos.y / this.size.height) * -2 + 1);
}
start_rendering(): void {
if (this.animation_frame_handle == undefined) {
this.schedule_render();
this.animation_frame_handle = requestAnimationFrame(this.call_render);
}
}
stop_rendering(): void {
if (this.animation_frame_handle != undefined) {
cancelAnimationFrame(this.animation_frame_handle);
this.animation_frame_handle = undefined;
}
}
schedule_render = (): void => {
this.render_scheduled = true;
};
reset_camera(position: Vector3, look_at: Vector3): void {
this.controls.setLookAt(
position.x,
position.y,
position.z,
look_at.x,
look_at.y,
look_at.z,
);
}
dispose(): void {
this.renderer.dispose();
this.controls.dispose();
}
protected init_camera_controls(): void {
(this.controls as CameraControls) = new CameraControls(
this.camera,
this.renderer.domElement,
);
this.controls.dampingFactor = 1;
this.controls.draggingDampingFactor = 1;
}
protected render(): void {
this.renderer.render(this.scene, this.camera);
}
private on_mouse_down = (e: Event): void => {
if (e.currentTarget) (e.currentTarget as HTMLElement).focus();
};
private call_render = (): void => {
const controls_updated = this.controls.update(this.controls_clock.getDelta());
const should_render = this.render_scheduled || controls_updated;
this.render_scheduled = false;
if (should_render) {
this.render();
}
this.animation_frame_handle = requestAnimationFrame(this.call_render);
};
}