mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Improved panning with perspective camera.
This commit is contained in:
parent
64daaf8fd2
commit
ff31c1ad27
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user