Track unit vectors to make linear approximations of deformations

This commit is contained in:
loovjo 2023-07-23 21:02:31 +02:00
parent 68816b0ea4
commit ce6a6ae8b5

198
xen.ts
View File

@ -33,6 +33,14 @@ class Vec2 {
normalize(): Vec2 { normalize(): Vec2 {
return this.div(this.length()); return this.div(this.length());
} }
cos_angle_to(other: Vec2): number {
return this.dot(other) / (this.length() * other.length());
}
angle_to(other: Vec2): number {
return Math.acos(this.cos_angle_to(other));
}
} }
class Vec3 { class Vec3 {
@ -74,6 +82,14 @@ class Vec3 {
return this.div(this.length()); return this.div(this.length());
} }
cos_angle_to(other: Vec3): number {
return this.dot(other) / (this.length() * other.length());
}
angle_to(other: Vec3): number {
return Math.acos(this.cos_angle_to(other));
}
// maps angle 0 to angle phi // maps angle 0 to angle phi
rotate_about_z(phi: number): Vec3 { rotate_about_z(phi: number): Vec3 {
let sp = Math.sin(phi); let sp = Math.sin(phi);
@ -98,10 +114,15 @@ class Vec3 {
} }
interface ProjectedPoint { interface ProjectedPoint {
screen_space: Vec2, // [0, width[x[0, height[
camera_space: Vec2, // [-1, 1]x[-1, 1] camera_space: Vec2, // [-1, 1]x[-1, 1]
distance: number, distance: number, // world units
in_view: boolean, // outside view? screen_margin: Vec2, // pixels, negative = outside of screen
behind: boolean, behind: boolean,
xhat_screen: Vec2,
yhat_screen: Vec2,
zhat_screen: Vec2,
} }
class CameraProjection { class CameraProjection {
@ -109,12 +130,16 @@ class CameraProjection {
yaw: number; yaw: number;
pitch: number; pitch: number;
fov: number; fov: number;
width: number;
height: number;
constructor(origin: Vec3, yaw: number, pitch: number, fov: number) { constructor(origin: Vec3, yaw: number, pitch: number, fov: number, width: number, height: number) {
this.origin = origin; this.origin = origin;
this.yaw = yaw; this.yaw = yaw;
this.pitch = pitch; this.pitch = pitch;
this.fov = fov; this.fov = fov;
this.width = width;
this.height = height;
} }
world_to_camera(point: Vec3, respect_origin: boolean): Vec3 { world_to_camera(point: Vec3, respect_origin: boolean): Vec3 {
@ -136,16 +161,66 @@ class CameraProjection {
let camera_space_3 = this.world_to_camera(point, respect_origin); let camera_space_3 = this.world_to_camera(point, respect_origin);
let scale = 1 / Math.tan(this.fov / 2); let scale = 1 / Math.tan(this.fov / 2);
let camera_space = new Vec2(scale * camera_space_3.x / camera_space_3.y, scale * camera_space_3.z / camera_space_3.y); let camera_space = new Vec2(scale * camera_space_3.x / camera_space_3.y, scale * camera_space_3.z / camera_space_3.y);
let screen_space = new Vec2(this.width / 2 + camera_space.x / 2 * this.width, this.height / 2 - camera_space.y / 2 * this.width);
let self = this;
function vec_screen(origin: Vec3, v: Vec3) {
// TODO: PLEASE DO THIS ANALYTICALLY FOR THE LOVE OF GOD
let h = 0.00001;
let camera_space_3 = self.world_to_camera(origin, true);
let camera_space = new Vec2(scale * camera_space_3.x / camera_space_3.y, scale * camera_space_3.z / camera_space_3.y);
let screen_space = new Vec2((0.5 * camera_space.x) * self.width, (-0.5 * camera_space.y) * self.width);
let v_camera_space_3 = self.world_to_camera(origin.add(v.mul(h)), true);
let v_camera_space = new Vec2(scale * v_camera_space_3.x / v_camera_space_3.y, scale * v_camera_space_3.z / v_camera_space_3.y);
let v_screen_space = new Vec2((0.5 * v_camera_space.x) * self.width, (-0.5 * v_camera_space.y) * self.width);
let diff = (v_screen_space.sub(screen_space)).div(h);
return diff;
}
let distance = camera_space_3.y; let distance = camera_space_3.y;
let behind = camera_space_3.y <= 0; let behind = camera_space_3.y <= 0;
let in_view = !behind;// && (camera_space.x >= -1 && camera_space.x <= 1 && camera_space.y >= -1 && camera_space.y < 1); let margin_x = Math.min(screen_space.x, this.width - screen_space.x);
let margin_y = Math.min(screen_space.y, this.height - screen_space.y);
// We want the
let origin_camera_space = camera_space_3;
if (camera_space.x < -1) {
// We want to move origin's x such that camera_space.x == -1
// scale * camera_space_3.x / camera_space_3.y == -1
// camera_space_3.x = -camera_space_3.y / scale
origin_camera_space.x = -camera_space_3.y / scale;
}
// All of these are similar
if (camera_space.x > 1) {
origin_camera_space.x = camera_space_3.y / scale;
}
if (camera_space.y < -1) {
origin_camera_space.z = -camera_space_3.y / scale;
}
if (camera_space.y > 1) {
origin_camera_space.z = camera_space_3.y / scale;
}
let origin = this.camera_to_world(origin_camera_space, true);
return { return {
screen_space: screen_space,
camera_space: camera_space, camera_space: camera_space,
distance: distance, distance: distance,
in_view: in_view, screen_margin: new Vec2(margin_x, margin_y),
behind: behind, behind: behind,
xhat_screen: vec_screen(origin, new Vec3(1, 0, 0)),
yhat_screen: vec_screen(origin, new Vec3(0, 1, 0)),
zhat_screen: vec_screen(origin, new Vec3(0, 0, 1)),
}; };
} }
view_vector(): Vec3 {
let yhat = new Vec3(0, 1, 0);
return yhat.rotate_about_x(this.pitch).rotate_about_z(this.yaw);
}
} }
type RGB = {r: number, g: number, b: number}; type RGB = {r: number, g: number, b: number};
@ -158,35 +233,84 @@ interface Drawable {
class Ball { class Ball {
at: Vec3; at: Vec3;
color: RGB; color: RGB;
constructor(at: Vec3, color: RGB) { size: number;
constructor(at: Vec3, color: RGB, size: number) {
this.at = at; this.at = at;
this.color = color; this.color = color;
this.size = size;
} }
render(at: ProjectedPoint, cam: CameraProjection, ctx: CanvasRenderingContext2D): boolean { render(at: ProjectedPoint, cam: CameraProjection, ctx: CanvasRenderingContext2D): boolean {
let x_amount = Math.sqrt(Math.pow(at.xhat_screen.x, 2) + Math.pow(at.yhat_screen.x, 2) + Math.pow(at.zhat_screen.x, 2));
let y_amount = Math.sqrt(Math.pow(at.xhat_screen.y, 2) + Math.pow(at.yhat_screen.y, 2) + Math.pow(at.zhat_screen.y, 2));
for (let a = 0; a < 2; a++) { for (let a = 0; a < 2; a++) {
ctx.fillStyle = `rgb(${this.color.r * a}, ${this.color.g * a}, ${this.color.b * a})`; ctx.fillStyle = `rgb(${this.color.r * a}, ${this.color.g * a}, ${this.color.b * a})`;
ctx.beginPath(); ctx.beginPath();
ctx.arc((at.camera_space.x / 2 + 0.5) * width, (-at.camera_space.y / 2 + 0.5) * height, 100 / at.distance + 1 - a, 0, Math.PI * 2); ctx.ellipse(
at.screen_space.x, at.screen_space.y,
this.size * x_amount + 1 - a, this.size * y_amount + 1 - a,
0,
0, Math.PI * 2
);
ctx.fill(); ctx.fill();
} }
return true; return true;
} }
} }
class Shadow {
at: Vec3;
size: number;
constructor(at: Vec2, size: number) {
this.at = new Vec3(at.x, at.y, 0);
this.size = size;
}
up(): Vec3 {
return new Vec3(0, 0, 1);
}
render(at: ProjectedPoint, cam: CameraProjection, ctx: CanvasRenderingContext2D): boolean {
// in world space, we have x_w^2 + y_w^2 = r^2
// in screen space, [ x_w y_w z_w ] is transformed to [ x_s y_s ]
// by the basis vectors x->at.xhat_screen, y->at.yhat_screen, z->at.zhat_screen
// x_s = x_s_hat * x_w + y_s_hat * y_w + z_s_hat * z_w
// TODO: Actually use this to render the ellipse
ctx.fillStyle = "black";
ctx.beginPath();
let x_amount = Math.sqrt(Math.pow(at.xhat_screen.x, 2) + Math.pow(at.yhat_screen.x, 2));
let y_amount = Math.sqrt(Math.pow(at.xhat_screen.y, 2) + Math.pow(at.yhat_screen.y, 2));
ctx.ellipse(
at.screen_space.x, at.screen_space.y,
this.size * x_amount,
this.size * y_amount,
0, // rotation
0, Math.PI * 2,
);
ctx.fill();
return true;
}
}
const FPS = 60; const FPS = 60;
let should_draw_gizmo = false; let should_draw_gizmo = false;
let canvas = document.getElementById("frame") as HTMLCanvasElement; let canvas = document.getElementById("frame") as HTMLCanvasElement;
let width = window.innerWidth; let width = window.innerWidth;
let height = window.innerHeight; let height = window.innerHeight;
width = Math.min(width, height); //width = Math.min(width, height);
height = width; //height = width;
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
canvas.style = `width: ${width}px; height: ${height}px;`; canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
let ctx: CanvasRenderingContext2D = canvas.getContext("2d") as CanvasRenderingContext2D; let ctx: CanvasRenderingContext2D = canvas.getContext("2d") as CanvasRenderingContext2D;
let camera = new CameraProjection(new Vec3(0, -10, 5), 0, -0.5, 60 * Math.PI / 180 * 1); let at = new Vec3(0, -10, 5);
let camera = new CameraProjection(at, 0, Math.atan(at.z/at.y), 60 * Math.PI / 180 * 1, width, height);
let pressing = new Set(); let pressing = new Set();
@ -248,21 +372,25 @@ function step() {
} }
} }
let objects: Array<Drawable> = []; function get_objects(t): Array<Drawable> {
for (let x = 5; x >= -5; x--) { let objects: Array<Drawable> = [];
for (let y = 5; y >= -5; y--) { for (let x = 5; x >= -5; x--) {
for (let z = -1; z <= 1; z += 2) { for (let y = 5; y >= -5; y--) {
objects.push(new Ball(new Vec3(x, y, z), {r: 100+x*10, g: 100+y*10, b: 100+50*z})); objects.push(new Ball(new Vec3(x, y, 1), {r: 100+x*10, g: 100+y*10, b: 100}, 0.3));
objects.push(new Shadow(new Vec2(x, y), 0.3));
} }
} }
return objects;
} }
let start = new Date();
function redraw(camera: CameraProjection) { function redraw(camera: CameraProjection) {
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
let objects = get_objects(new Date().getTime() - start.getTime());
let rendered = objects let rendered = objects
.map(o => {return {object: o, at: camera.project(o.at, true )}}) .map(o => {return {object: o, at: camera.project(o.at, true )}})
.filter(r => r.at.in_view) .filter(r => !r.at.behind)
.sort((a, b) => -(a.at.distance - b.at.distance)); .sort((a, b) => -(a.at.distance - b.at.distance));
for (let x of rendered) { for (let x of rendered) {
@ -270,33 +398,33 @@ function redraw(camera: CameraProjection) {
} }
} }
const DIRECTIONS = [
{dir: new Vec3(1, 0, 0), color: "rgb(0, 0, 255)"},
{dir: new Vec3(0, 1, 0), color: "rgb(0, 180, 0)"},
{dir: new Vec3(0, 0, 1), color: "rgb(255, 0, 0)"},
];
function draw_gizmo(ctx: CanvasRenderingContext2D, cam: CameraProjection) { function draw_gizmo(ctx: CanvasRenderingContext2D, cam: CameraProjection) {
let size = 100; let size = 0.6;
let at = new Vec2(width / 2, width / 2); let distance_away = 3;
let old_fov = camera.fov; let v = new Vec3(0.0, distance_away, 0.0).rotate_about_x(camera.pitch).rotate_about_z(camera.yaw);
camera.fov = Math.PI / 4; let gizmo_world = cam.origin.add(v);
// temporarily set the FOV to a known value so the gizmo doesn't depend on the camera FOV
let old_fov = cam.fov;
//camera.fov = Math.PI / 4;
let distance = 100; let gizmo_screen = cam.project(gizmo_world, true);
let gizmo_at = camera.camera_to_world(new Vec3(0, distance, 0), false);
for (let direction of DIRECTIONS) { let directions = [
let gizmo_up = gizmo_at.add(direction.dir); { dir: gizmo_screen.xhat_screen, color: "rgb(0, 0, 255)" },
let up_cam = camera.project(gizmo_up, false); { dir: gizmo_screen.yhat_screen, color: "rgb(0, 180, 0)" },
let xy = up_cam.camera_space.mul(distance); { dir: gizmo_screen.zhat_screen, color: "rgb(255, 0, 0)" },
xy = xy.mul(size / 3); ];
for (let direction of directions) {
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = direction.color; ctx.strokeStyle = direction.color;
ctx.lineWidth = 4; ctx.lineWidth = 4;
ctx.moveTo(at.x, at.y); let start = gizmo_screen.screen_space;
ctx.lineTo(at.x + xy.x, at.y - xy.y); let end = gizmo_screen.screen_space.add(direction.dir.mul(size));
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke(); ctx.stroke();
} }