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 { export class Vec3 {
constructor(public x: number, public y: number, public z: number) {} 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; 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 { clone(): Mat4 {
return new Mat4(new Float32Array(this.data)); 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 { 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 readonly look_at: Vec3 = new Vec3(0, 0, 0);
private x_rot: number = 0; private x_rot: number = 0;
private y_rot: number = 0; private y_rot: number = 0;
private z_rot: number = 0; private z_rot: number = 0;
private _zoom: number = 1; private _zoom: number = 1;
private readonly _mat4 = Mat4.identity();
get mat4(): Mat4 { readonly view_mat4 = Mat4.identity();
return this._mat4; 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 { 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.x += x;
this.look_at.y += y; this.look_at.y += y;
this.look_at.z += z;
this.update_matrix(); this.update_matrix();
return this; return this;
} }
@ -25,6 +102,9 @@ export class Camera {
*/ */
zoom(factor: number): this { zoom(factor: number): this {
this._zoom *= factor; this._zoom *= factor;
this.position.x *= factor;
this.position.y *= factor;
this.position.z *= factor;
this.look_at.x *= factor; this.look_at.x *= factor;
this.look_at.y *= factor; this.look_at.y *= factor;
this.look_at.z *= factor; this.look_at.z *= factor;
@ -33,6 +113,9 @@ export class Camera {
} }
reset(): this { reset(): this {
this.position.x = 0;
this.position.y = 0;
this.position.z = 0;
this.look_at.x = 0; this.look_at.x = 0;
this.look_at.y = 0; this.look_at.y = 0;
this.look_at.z = 0; this.look_at.z = 0;
@ -45,11 +128,11 @@ export class Camera {
} }
private update_matrix(): void { private update_matrix(): void {
this._mat4.data[12] = -this.look_at.x; this.view_mat4.data[12] = this._zoom * -this.position.x;
this._mat4.data[13] = -this.look_at.y; this.view_mat4.data[13] = this._zoom * -this.position.y;
this._mat4.data[14] = -this.look_at.z; this.view_mat4.data[14] = this._zoom * -this.position.z;
this._mat4.data[0] = this._zoom; this.view_mat4.data[0] = this._zoom;
this._mat4.data[5] = this._zoom; this.view_mat4.data[5] = this._zoom;
this._mat4.data[10] = this._zoom; this.view_mat4.data[10] = this._zoom;
} }
} }

View File

@ -1,9 +1,8 @@
import { Renderer } from "./Renderer"; import { Renderer } from "./Renderer";
import { Scene } from "./Scene"; import { Scene } from "./Scene";
import { Camera } from "./Camera"; import { Camera, Projection } from "./Camera";
import { Gfx } from "./Gfx"; import { Gfx } from "./Gfx";
import { Mat4, Vec2, vec2_diff } from "../math/linear_algebra"; import { Mat4, Vec2, vec2_diff } from "../math/linear_algebra";
import { deg_to_rad } from "../math";
export abstract class GfxRenderer implements Renderer { export abstract class GfxRenderer implements Renderer {
private pointer_pos?: Vec2; private pointer_pos?: Vec2;
@ -12,18 +11,18 @@ export abstract class GfxRenderer implements Renderer {
*/ */
private animation_frame?: number; private animation_frame?: number;
protected projection_mat: Mat4 = Mat4.identity();
abstract readonly gfx: Gfx; abstract readonly gfx: Gfx;
readonly scene = new Scene(); readonly scene = new Scene();
readonly camera = new Camera(); readonly camera: Camera;
readonly canvas_element: HTMLCanvasElement = document.createElement("canvas"); 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.width = 800;
this.canvas_element.height = 600; this.canvas_element.height = 600;
this.canvas_element.addEventListener("mousedown", this.mousedown); this.canvas_element.addEventListener("mousedown", this.mousedown);
this.canvas_element.addEventListener("wheel", this.wheel, { passive: true }); this.canvas_element.addEventListener("wheel", this.wheel, { passive: true });
this.camera = new Camera(this.canvas_element.width, this.canvas_element.height, projection);
} }
dispose(): void { dispose(): void {
@ -31,39 +30,7 @@ export abstract class GfxRenderer implements Renderer {
} }
set_size(width: number, height: number): void { set_size(width: number, height: number): void {
if (this.perspective_projection) { this.camera.set_viewport(width, height);
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.schedule_render(); this.schedule_render();
} }
@ -133,18 +100,22 @@ export abstract class GfxRenderer implements Renderer {
}; };
private wheel = (evt: WheelEvent): void => { private wheel = (evt: WheelEvent): void => {
if (this.perspective_projection) { switch (this.camera.projection) {
if (evt.deltaY < 0) { case Projection.Orthographic:
this.camera.pan(0, 0, -5);
} else {
this.camera.pan(0, 0, 5);
}
} else {
if (evt.deltaY < 0) { if (evt.deltaY < 0) {
this.camera.zoom(1.1); this.camera.zoom(1.1);
} else { } else {
this.camera.zoom(0.9); 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(); 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 pos_tex_frag_shader_source from "./pos_tex.frag";
import { GfxRenderer } from "../GfxRenderer"; import { GfxRenderer } from "../GfxRenderer";
import { WebglGfx, WebglMesh } from "./WebglGfx"; import { WebglGfx, WebglMesh } from "./WebglGfx";
import { Projection } from "../Camera";
export class WebglRenderer extends GfxRenderer { export class WebglRenderer extends GfxRenderer {
private readonly gl: WebGL2RenderingContext; private readonly gl: WebGL2RenderingContext;
@ -13,8 +14,8 @@ export class WebglRenderer extends GfxRenderer {
readonly gfx: WebglGfx; readonly gfx: WebglGfx;
constructor(perspective_projection: boolean) { constructor(projection: Projection) {
super(perspective_projection); super(projection);
const gl = this.canvas_element.getContext("webgl2"); const gl = this.canvas_element.getContext("webgl2");
@ -65,7 +66,7 @@ export class WebglRenderer extends GfxRenderer {
const program = this.shader_programs[node.mesh.format]; const program = this.shader_programs[node.mesh.format];
program.bind(); 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_camera_uniform(mat);
program.set_mat_normal_uniform(mat.normal_mat3()); program.set_mat_normal_uniform(mat.normal_mat3());
@ -86,6 +87,6 @@ export class WebglRenderer extends GfxRenderer {
} }
return mat; 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 { WebgpuGfx, WebgpuMesh } from "./WebgpuGfx";
import { ShaderLoader } from "./ShaderLoader"; import { ShaderLoader } from "./ShaderLoader";
import { HttpClient } from "../../HttpClient"; import { HttpClient } from "../../HttpClient";
import { Projection } from "../Camera";
const logger = LogManager.get("core/rendering/webgpu/WebgpuRenderer"); const logger = LogManager.get("core/rendering/webgpu/WebgpuRenderer");
@ -30,8 +31,8 @@ export class WebgpuRenderer extends GfxRenderer {
return this.gpu!.gfx; return this.gpu!.gfx;
} }
constructor(perspective_projection: boolean, http_client: HttpClient) { constructor(projection: Projection, http_client: HttpClient) {
super(perspective_projection); super(projection);
this.shader_loader = new ShaderLoader(http_client); this.shader_loader = new ShaderLoader(http_client);
@ -175,7 +176,7 @@ export class WebgpuRenderer extends GfxRenderer {
pass_encoder.setPipeline(pipeline); 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) => { this.scene.traverse((node, parent_mat) => {
const mat = mat4_product(parent_mat, node.transform); 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 { Random } from "../core/Random";
import { Renderer } from "../core/rendering/Renderer"; import { Renderer } from "../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer"; import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
import { Projection } from "../core/rendering/Camera";
export function initialize_viewer( export function initialize_viewer(
http_client: HttpClient, http_client: HttpClient,
@ -48,12 +49,15 @@ export function initialize_viewer(
const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer"); const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer");
const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer"); 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")) { } else if (gui_store.feature_active("webgl")) {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer"); const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer"); const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer");
renderer = new ModelGfxRenderer(store, new WebglRenderer(true)); renderer = new ModelGfxRenderer(store, new WebglRenderer(Projection.Perspective));
} else { } else {
const { ModelRenderer } = await import("./rendering/ModelRenderer"); const { ModelRenderer } = await import("./rendering/ModelRenderer");
@ -79,10 +83,16 @@ export function initialize_viewer(
if (gui_store.feature_active("webgpu")) { if (gui_store.feature_active("webgpu")) {
const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer"); 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 { } else {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer"); 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); 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) { constructor(private readonly store: ModelStore, private readonly renderer: GfxRenderer) {
this.canvas_element = renderer.canvas_element; 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)); 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 { GfxRenderer } from "../../../../src/core/rendering/GfxRenderer";
import { Gfx } from "../../../../src/core/rendering/Gfx"; import { Gfx } from "../../../../src/core/rendering/Gfx";
import { Projection } from "../../../../src/core/rendering/Camera";
export class StubGfxRenderer extends GfxRenderer { export class StubGfxRenderer extends GfxRenderer {
get gfx(): Gfx { get gfx(): Gfx {
@ -7,7 +8,7 @@ export class StubGfxRenderer extends GfxRenderer {
} }
constructor() { constructor() {
super(false); super(Projection.Orthographic);
} }
protected render(): void {} // eslint-disable-line protected render(): void {} // eslint-disable-line