Improved panning with perspective camera.

This commit is contained in:
Daan Vanden Bosch 2020-01-29 17:36:15 +01:00
parent 64daaf8fd2
commit ff31c1ad27
8 changed files with 188 additions and 75 deletions

View File

@ -19,6 +19,24 @@ export function vec2_diff(v: Vec2, w: Vec2): Vec2 {
export class Vec3 {
constructor(public x: number, public y: number, public z: number) {}
magnitude(): number {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
}
export function vec3_diff(v: Vec3, w: Vec3): Vec3 {
return new Vec3(v.x - w.x, v.y - w.y, v.z - v.z);
}
/**
* Computes the distance between points p and q. Equivalent to `vec3_diff(p, q).magnitude()`.
*/
export function vec3_dist(p: Vec3, q: Vec3): number {
const x = p.x - q.x;
const y = p.y - q.y;
const z = p.z - q.z;
return Math.sqrt(x * x + y * y + z * z);
}
/**
@ -275,6 +293,34 @@ export class Mat4 {
this.data[i + j * 4] = value;
}
// prettier-ignore
set_all(
m00: number, m01: number, m02: number, m03: number,
m10: number, m11: number, m12: number, m13: number,
m20: number, m21: number, m22: number, m23: number,
m30: number, m31: number, m32: number, m33: number,
):void {
this.data[0] = m00;
this.data[1] = m10;
this.data[2] = m20;
this.data[3] = m30;
this.data[4] = m01;
this.data[5] = m11;
this.data[6] = m21;
this.data[7] = m31;
this.data[8] = m02;
this.data[9] = m12;
this.data[10] = m22;
this.data[11] = m32;
this.data[12] = m03;
this.data[13] = m13;
this.data[14] = m23;
this.data[15] = m33;
}
clone(): Mat4 {
return new Mat4(new Float32Array(this.data));
}

View File

@ -1,21 +1,98 @@
import { Mat4, Vec3 } from "../math/linear_algebra";
import { Mat4, Vec3, vec3_dist } from "../math/linear_algebra";
import { deg_to_rad } from "../math";
export enum Projection {
Orthographic,
Perspective,
}
export class Camera {
// Only applicable in perspective mode.
private readonly fov = deg_to_rad(75);
private readonly position: Vec3 = new Vec3(0, 0, 0);
private readonly look_at: Vec3 = new Vec3(0, 0, 0);
private x_rot: number = 0;
private y_rot: number = 0;
private z_rot: number = 0;
private _zoom: number = 1;
private readonly _mat4 = Mat4.identity();
get mat4(): Mat4 {
return this._mat4;
readonly view_mat4 = Mat4.identity();
readonly projection_mat4 = Mat4.identity();
/**
* Effective field of view in radians. Only applicable in perspective mode.
*/
get effective_fov(): number {
return 2 * Math.atan(Math.tan(0.5 * this.fov) / this._zoom);
}
constructor(
private viewport_width: number,
private viewport_height: number,
readonly projection: Projection,
) {
this.set_viewport(viewport_width, viewport_height);
}
set_viewport(width: number, height: number): void {
this.viewport_width = width;
this.viewport_height = height;
switch (this.projection) {
case Projection.Orthographic:
{
const w = width;
const h = height;
const n = -1000;
const f = 1000;
// prettier-ignore
this.projection_mat4.set_all(
2/w, 0, 0, 0,
0, 2/h, 0, 0,
0, 0, 2/(n-f), 0,
0, 0, 0, 1,
);
}
break;
case Projection.Perspective:
{
const aspect = width / height;
const n = 0.1;
const f = 2000;
const t = n * Math.tan(0.5 * this.fov);
const b = -t;
const r = aspect * t;
const l = -r;
// prettier-ignore
this.projection_mat4.set_all(
2*n / (r-l), 0, (l+r) / (l-r), 0,
0, 2*n / (t-b), (b+t) / (b-t), 0,
0, 0, (f+n) / (n-f), (2*f*n) / (f-n),
0, 0, 1, 0,
);
}
break;
}
}
pan(x: number, y: number, z: number): this {
const pan_factor =
(3 * vec3_dist(this.position, this.look_at) * Math.tan(0.5 * this.effective_fov)) /
this.viewport_width;
x *= pan_factor;
y *= pan_factor;
this.position.x += x;
this.position.y += y;
this.position.z += z;
this.look_at.x += x;
this.look_at.y += y;
this.look_at.z += z;
this.update_matrix();
return this;
}
@ -25,6 +102,9 @@ export class Camera {
*/
zoom(factor: number): this {
this._zoom *= factor;
this.position.x *= factor;
this.position.y *= factor;
this.position.z *= factor;
this.look_at.x *= factor;
this.look_at.y *= factor;
this.look_at.z *= factor;
@ -33,6 +113,9 @@ export class Camera {
}
reset(): this {
this.position.x = 0;
this.position.y = 0;
this.position.z = 0;
this.look_at.x = 0;
this.look_at.y = 0;
this.look_at.z = 0;
@ -45,11 +128,11 @@ export class Camera {
}
private update_matrix(): void {
this._mat4.data[12] = -this.look_at.x;
this._mat4.data[13] = -this.look_at.y;
this._mat4.data[14] = -this.look_at.z;
this._mat4.data[0] = this._zoom;
this._mat4.data[5] = this._zoom;
this._mat4.data[10] = this._zoom;
this.view_mat4.data[12] = this._zoom * -this.position.x;
this.view_mat4.data[13] = this._zoom * -this.position.y;
this.view_mat4.data[14] = this._zoom * -this.position.z;
this.view_mat4.data[0] = this._zoom;
this.view_mat4.data[5] = this._zoom;
this.view_mat4.data[10] = this._zoom;
}
}

View File

@ -1,9 +1,8 @@
import { Renderer } from "./Renderer";
import { Scene } from "./Scene";
import { Camera } from "./Camera";
import { Camera, Projection } from "./Camera";
import { Gfx } from "./Gfx";
import { Mat4, Vec2, vec2_diff } from "../math/linear_algebra";
import { deg_to_rad } from "../math";
export abstract class GfxRenderer implements Renderer {
private pointer_pos?: Vec2;
@ -12,18 +11,18 @@ export abstract class GfxRenderer implements Renderer {
*/
private animation_frame?: number;
protected projection_mat: Mat4 = Mat4.identity();
abstract readonly gfx: Gfx;
readonly scene = new Scene();
readonly camera = new Camera();
readonly camera: Camera;
readonly canvas_element: HTMLCanvasElement = document.createElement("canvas");
protected constructor(private readonly perspective_projection: boolean) {
protected constructor(projection: Projection) {
this.canvas_element.width = 800;
this.canvas_element.height = 600;
this.canvas_element.addEventListener("mousedown", this.mousedown);
this.canvas_element.addEventListener("wheel", this.wheel, { passive: true });
this.camera = new Camera(this.canvas_element.width, this.canvas_element.height, projection);
}
dispose(): void {
@ -31,39 +30,7 @@ export abstract class GfxRenderer implements Renderer {
}
set_size(width: number, height: number): void {
if (this.perspective_projection) {
const fov = 75;
const aspect = width / height;
const n = 0.1;
const f = 2000;
const t = n * Math.tan(deg_to_rad(0.5 * fov));
const b = -t;
const r = aspect * t;
const l = -r;
// prettier-ignore
this.projection_mat = Mat4.of(
2*n / (r-l), 0, (r+l) / (r-l), 0,
0, 2*n / (t-b), (t+b) / (t-b), 0,
0, 0, -(f+n) / (f-n), -(2*f*n) / (f-n),
0, 0, -1, 0,
);
} else {
const w = width;
const h = height;
const n = -1000;
const f = 1000;
// prettier-ignore
this.projection_mat = Mat4.of(
2/w, 0, 0, 0,
0, 2/h, 0, 0,
0, 0, 2/(n-f), 0,
0, 0, 0, 1,
);
}
this.camera.set_viewport(width, height);
this.schedule_render();
}
@ -133,18 +100,22 @@ export abstract class GfxRenderer implements Renderer {
};
private wheel = (evt: WheelEvent): void => {
if (this.perspective_projection) {
if (evt.deltaY < 0) {
this.camera.pan(0, 0, -5);
} else {
this.camera.pan(0, 0, 5);
}
} else {
if (evt.deltaY < 0) {
this.camera.zoom(1.1);
} else {
this.camera.zoom(0.9);
}
switch (this.camera.projection) {
case Projection.Orthographic:
if (evt.deltaY < 0) {
this.camera.zoom(1.1);
} else {
this.camera.zoom(0.9);
}
break;
case Projection.Perspective:
if (evt.deltaY < 0) {
this.camera.pan(0, 0, 5);
} else {
this.camera.pan(0, 0, -5);
}
break;
}
this.schedule_render();

View File

@ -6,6 +6,7 @@ import pos_tex_vert_shader_source from "./pos_tex.vert";
import pos_tex_frag_shader_source from "./pos_tex.frag";
import { GfxRenderer } from "../GfxRenderer";
import { WebglGfx, WebglMesh } from "./WebglGfx";
import { Projection } from "../Camera";
export class WebglRenderer extends GfxRenderer {
private readonly gl: WebGL2RenderingContext;
@ -13,8 +14,8 @@ export class WebglRenderer extends GfxRenderer {
readonly gfx: WebglGfx;
constructor(perspective_projection: boolean) {
super(perspective_projection);
constructor(projection: Projection) {
super(projection);
const gl = this.canvas_element.getContext("webgl2");
@ -65,7 +66,7 @@ export class WebglRenderer extends GfxRenderer {
const program = this.shader_programs[node.mesh.format];
program.bind();
program.set_mat_projection_uniform(this.projection_mat);
program.set_mat_projection_uniform(this.camera.projection_mat4);
program.set_mat_camera_uniform(mat);
program.set_mat_normal_uniform(mat.normal_mat3());
@ -86,6 +87,6 @@ export class WebglRenderer extends GfxRenderer {
}
return mat;
}, this.camera.mat4);
}, this.camera.view_mat4);
}
}

View File

@ -5,6 +5,7 @@ import { mat4_product } from "../../math/linear_algebra";
import { WebgpuGfx, WebgpuMesh } from "./WebgpuGfx";
import { ShaderLoader } from "./ShaderLoader";
import { HttpClient } from "../../HttpClient";
import { Projection } from "../Camera";
const logger = LogManager.get("core/rendering/webgpu/WebgpuRenderer");
@ -30,8 +31,8 @@ export class WebgpuRenderer extends GfxRenderer {
return this.gpu!.gfx;
}
constructor(perspective_projection: boolean, http_client: HttpClient) {
super(perspective_projection);
constructor(projection: Projection, http_client: HttpClient) {
super(projection);
this.shader_loader = new ShaderLoader(http_client);
@ -175,7 +176,7 @@ export class WebgpuRenderer extends GfxRenderer {
pass_encoder.setPipeline(pipeline);
const camera_project_mat = mat4_product(this.projection_mat, this.camera.mat4);
const camera_project_mat = mat4_product(this.camera.projection_mat4, this.camera.view_mat4);
this.scene.traverse((node, parent_mat) => {
const mat = mat4_product(parent_mat, node.transform);

View File

@ -6,6 +6,7 @@ import { Disposer } from "../core/observable/Disposer";
import { Random } from "../core/Random";
import { Renderer } from "../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
import { Projection } from "../core/rendering/Camera";
export function initialize_viewer(
http_client: HttpClient,
@ -48,12 +49,15 @@ export function initialize_viewer(
const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer");
const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer");
renderer = new ModelGfxRenderer(store, new WebgpuRenderer(true, http_client));
renderer = new ModelGfxRenderer(
store,
new WebgpuRenderer(Projection.Perspective, http_client),
);
} else if (gui_store.feature_active("webgl")) {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer");
renderer = new ModelGfxRenderer(store, new WebglRenderer(true));
renderer = new ModelGfxRenderer(store, new WebglRenderer(Projection.Perspective));
} else {
const { ModelRenderer } = await import("./rendering/ModelRenderer");
@ -79,10 +83,16 @@ export function initialize_viewer(
if (gui_store.feature_active("webgpu")) {
const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer");
renderer = new TextureRenderer(controller, new WebgpuRenderer(false, http_client));
renderer = new TextureRenderer(
controller,
new WebgpuRenderer(Projection.Orthographic, http_client),
);
} else {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
renderer = new TextureRenderer(controller, new WebglRenderer(false));
renderer = new TextureRenderer(
controller,
new WebglRenderer(Projection.Orthographic),
);
}
return new TextureView(controller, renderer);

View File

@ -12,7 +12,7 @@ export class ModelGfxRenderer implements Renderer {
constructor(private readonly store: ModelStore, private readonly renderer: GfxRenderer) {
this.canvas_element = renderer.canvas_element;
renderer.camera.pan(0, 0, 50);
renderer.camera.pan(0, 0, -50);
this.disposer.add_all(store.current_nj_object.observe(this.nj_object_or_xvm_changed));
}

View File

@ -1,5 +1,6 @@
import { GfxRenderer } from "../../../../src/core/rendering/GfxRenderer";
import { Gfx } from "../../../../src/core/rendering/Gfx";
import { Projection } from "../../../../src/core/rendering/Camera";
export class StubGfxRenderer extends GfxRenderer {
get gfx(): Gfx {
@ -7,7 +8,7 @@ export class StubGfxRenderer extends GfxRenderer {
}
constructor() {
super(false);
super(Projection.Orthographic);
}
protected render(): void {} // eslint-disable-line