diff --git a/demo/client/src/3d-demo.ts b/demo/client/src/3d-demo.ts index 921bf5b7..777a789e 100644 --- a/demo/client/src/3d-demo.ts +++ b/demo/client/src/3d-demo.ts @@ -12,6 +12,7 @@ import * as glmatrix from 'gl-matrix'; import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy"; import {DemoAppController} from "./app-controller"; +import {PerspectiveCamera} from "./camera"; import {mat4, vec2} from "gl-matrix"; import {PathfinderMeshData} from "./meshes"; import {ShaderMap, ShaderProgramSource} from "./shader-loader"; @@ -19,7 +20,6 @@ import {BUILTIN_FONT_URI, TextLayout, PathfinderGlyph} from "./text"; import {PathfinderError, panic, unwrapNull} from "./utils"; import {PathfinderDemoView, Timings} from "./view"; import SSAAStrategy from "./ssaa-strategy"; -import { OrthographicCamera } from "./camera"; const TEXT: string = "Lorem ipsum dolor sit amet"; @@ -27,6 +27,10 @@ const FONT: string = 'open-sans'; const PIXELS_PER_UNIT: number = 1.0; +const FOV: number = 45.0; +const NEAR_CLIP_PLANE: number = 0.01; +const FAR_CLIP_PLANE: number = 1000.0; + const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = { none: NoAAStrategy, ssaa: SSAAStrategy, @@ -82,9 +86,8 @@ class ThreeDView extends PathfinderDemoView { this.appController = appController; - this.camera = new OrthographicCamera(this.canvas); - this.camera.onPan = () => this.setDirty(); - this.camera.onZoom = () => this.setDirty(); + this.camera = new PerspectiveCamera(this.canvas); + this.camera.onChange = () => this.setDirty(); } uploadPathMetadata(pathCount: number) { @@ -140,9 +143,13 @@ class ThreeDView extends PathfinderDemoView { protected get worldTransform() { const transform = glmatrix.mat4.create(); - const translation = this.camera.translation; - glmatrix.mat4.fromTranslation(transform, [translation[0], translation[1], 0]); - glmatrix.mat4.scale(transform, transform, [this.camera.scale, this.camera.scale, 1.0]); + glmatrix.mat4.perspective(transform, + FOV / 180.0 * Math.PI, + this.canvas.width / this.canvas.height, + NEAR_CLIP_PLANE, + FAR_CLIP_PLANE); + glmatrix.mat4.translate(transform, transform, this.camera.translation); + glmatrix.mat4.mul(transform, transform, this.camera.rotationMatrix); return transform; } @@ -150,7 +157,7 @@ class ThreeDView extends PathfinderDemoView { private appController: ThreeDController; - camera: OrthographicCamera; + camera: PerspectiveCamera; } class ThreeDGlyph extends PathfinderGlyph { diff --git a/demo/client/src/camera.ts b/demo/client/src/camera.ts index f82e446d..db82a1b3 100644 --- a/demo/client/src/camera.ts +++ b/demo/client/src/camera.ts @@ -11,7 +11,12 @@ import * as glmatrix from 'gl-matrix'; import {PathfinderView} from "./view"; -const SCALE_FACTOR: number = 1.0 / 100.0; +const ORTHOGRAPHIC_ZOOM_SPEED: number = 1.0 / 100.0; + +const PERSPECTIVE_MOVEMENT_SPEED: number = 10.0; +const PERSPECTIVE_ROTATION_SPEED: number = 1.0 / 300.0; + +const MOVEMENT_INTERVAL_DELAY: number = 10; export abstract class Camera { constructor(canvas: HTMLCanvasElement) { @@ -49,7 +54,7 @@ export class OrthographicCamera extends Camera { glmatrix.vec2.sub(absoluteTranslation, this.translation, mouseLocation); glmatrix.vec2.scale(absoluteTranslation, absoluteTranslation, 1.0 / this.scale); - this.scale *= 1.0 - event.deltaY * window.devicePixelRatio * SCALE_FACTOR; + this.scale *= 1.0 - event.deltaY * window.devicePixelRatio * ORTHOGRAPHIC_ZOOM_SPEED; glmatrix.vec2.scale(absoluteTranslation, absoluteTranslation, this.scale); glmatrix.vec2.add(this.translation, absoluteTranslation, mouseLocation); @@ -73,3 +78,77 @@ export class OrthographicCamera extends Camera { translation: glmatrix.vec2; scale: number; } + +export class PerspectiveCamera extends Camera { + constructor(canvas: HTMLCanvasElement) { + super(canvas); + + this.translation = glmatrix.vec3.create(); + this.rotation = glmatrix.vec2.create(); + this.movementDelta = glmatrix.vec3.create(); + this.movementInterval = null; + + this.canvas.addEventListener('mousedown', event => this.onMouseDown(event), false); + this.canvas.addEventListener('mouseup', event => this.onMouseUp(event), false); + this.canvas.addEventListener('mousemove', event => this.onMouseMove(event), false); + + this.onChange = null; + } + + private onMouseDown(event: MouseEvent): void { + if (document.pointerLockElement !== this.canvas) { + this.canvas.requestPointerLock(); + return; + } + + this.movementDelta = glmatrix.vec3.fromValues(PERSPECTIVE_MOVEMENT_SPEED, 0.0, 0.0); + if (event.button !== 1) + this.movementDelta[0] = -this.movementDelta[0]; + + this.movementInterval = window.setInterval(() => this.move(), MOVEMENT_INTERVAL_DELAY); + } + + private onMouseUp(event: MouseEvent): void { + if (this.movementInterval != null) { + window.clearInterval(this.movementInterval); + this.movementDelta = glmatrix.vec3.create(); + } + } + + private move() { + const delta = glmatrix.vec3.clone(this.movementDelta); + glmatrix.vec3.transformMat4(delta, delta, this.rotationMatrix); + glmatrix.vec3.add(this.translation, this.translation, delta); + + if (this.onChange != null) + this.onChange(); + } + + private onMouseMove(event: MouseEvent): void { + if (document.pointerLockElement !== this.canvas) + return; + + this.rotation[0] += event.movementY * PERSPECTIVE_ROTATION_SPEED; + this.rotation[1] += event.movementX * PERSPECTIVE_ROTATION_SPEED; + + if (this.onChange != null) + this.onChange(); + } + + get rotationMatrix(): glmatrix.mat4 { + const matrix = glmatrix.mat4.create(); + glmatrix.mat4.fromXRotation(matrix, this.rotation[0]); + glmatrix.mat4.rotateY(matrix, matrix, this.rotation[1]); + return matrix; + } + + onChange: (() => void) | null; + + translation: glmatrix.vec3; + + /// Pitch and yaw Euler angles. + rotation: glmatrix.vec2; + + private movementDelta: glmatrix.vec3; + private movementInterval: number | null; +}