diff --git a/src/core/math/linear_algebra.ts b/src/core/math/linear_algebra.ts index 6d151c37..aada29f0 100644 --- a/src/core/math/linear_algebra.ts +++ b/src/core/math/linear_algebra.ts @@ -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)); } diff --git a/src/core/rendering/Camera.ts b/src/core/rendering/Camera.ts index 0b51d5ed..6a12f710 100644 --- a/src/core/rendering/Camera.ts +++ b/src/core/rendering/Camera.ts @@ -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; } } diff --git a/src/core/rendering/GfxRenderer.ts b/src/core/rendering/GfxRenderer.ts index 11d1a253..01a1c089 100644 --- a/src/core/rendering/GfxRenderer.ts +++ b/src/core/rendering/GfxRenderer.ts @@ -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(); diff --git a/src/core/rendering/webgl/WebglRenderer.ts b/src/core/rendering/webgl/WebglRenderer.ts index be1fc89a..5b2bc6c0 100644 --- a/src/core/rendering/webgl/WebglRenderer.ts +++ b/src/core/rendering/webgl/WebglRenderer.ts @@ -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); } } diff --git a/src/core/rendering/webgpu/WebgpuRenderer.ts b/src/core/rendering/webgpu/WebgpuRenderer.ts index b24cb180..becc1b79 100644 --- a/src/core/rendering/webgpu/WebgpuRenderer.ts +++ b/src/core/rendering/webgpu/WebgpuRenderer.ts @@ -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); diff --git a/src/viewer/index.ts b/src/viewer/index.ts index dc8b9faf..cc0bc451 100644 --- a/src/viewer/index.ts +++ b/src/viewer/index.ts @@ -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); diff --git a/src/viewer/rendering/ModelGfxRenderer.ts b/src/viewer/rendering/ModelGfxRenderer.ts index 7e19c3b5..2bda3673 100644 --- a/src/viewer/rendering/ModelGfxRenderer.ts +++ b/src/viewer/rendering/ModelGfxRenderer.ts @@ -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)); } diff --git a/test/src/core/rendering/StubGfxRenderer.ts b/test/src/core/rendering/StubGfxRenderer.ts index e6adf9bd..a440b6fa 100644 --- a/test/src/core/rendering/StubGfxRenderer.ts +++ b/test/src/core/rendering/StubGfxRenderer.ts @@ -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