diff --git a/xen.ts b/xen.ts index eb0cbea..d710e87 100644 --- a/xen.ts +++ b/xen.ts @@ -33,6 +33,14 @@ class Vec2 { normalize(): Vec2 { 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 { @@ -74,6 +82,14 @@ class Vec3 { 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 rotate_about_z(phi: number): Vec3 { let sp = Math.sin(phi); @@ -98,10 +114,15 @@ class Vec3 { } interface ProjectedPoint { + screen_space: Vec2, // [0, width[x[0, height[ camera_space: Vec2, // [-1, 1]x[-1, 1] - distance: number, - in_view: boolean, // outside view? + distance: number, // world units + screen_margin: Vec2, // pixels, negative = outside of screen behind: boolean, + + xhat_screen: Vec2, + yhat_screen: Vec2, + zhat_screen: Vec2, } class CameraProjection { @@ -109,12 +130,16 @@ class CameraProjection { yaw: number; pitch: 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.yaw = yaw; this.pitch = pitch; this.fov = fov; + this.width = width; + this.height = height; } 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 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 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 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 { + screen_space: screen_space, camera_space: camera_space, distance: distance, - in_view: in_view, + screen_margin: new Vec2(margin_x, margin_y), 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}; @@ -158,35 +233,84 @@ interface Drawable { class Ball { at: Vec3; color: RGB; - constructor(at: Vec3, color: RGB) { + size: number; + constructor(at: Vec3, color: RGB, size: number) { this.at = at; this.color = color; + this.size = size; } 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++) { ctx.fillStyle = `rgb(${this.color.r * a}, ${this.color.g * a}, ${this.color.b * a})`; 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(); } 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; let should_draw_gizmo = false; let canvas = document.getElementById("frame") as HTMLCanvasElement; let width = window.innerWidth; let height = window.innerHeight; -width = Math.min(width, height); -height = width; +//width = Math.min(width, height); +//height = width; canvas.width = width; 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 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(); @@ -248,21 +372,25 @@ function step() { } } -let objects: Array = []; -for (let x = 5; x >= -5; x--) { - for (let y = 5; y >= -5; y--) { - for (let z = -1; z <= 1; z += 2) { - objects.push(new Ball(new Vec3(x, y, z), {r: 100+x*10, g: 100+y*10, b: 100+50*z})); +function get_objects(t): Array { + let objects: Array = []; + for (let x = 5; x >= -5; x--) { + for (let y = 5; y >= -5; y--) { + 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) { ctx.clearRect(0, 0, width, height); + let objects = get_objects(new Date().getTime() - start.getTime()); let rendered = objects .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)); 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) { - let size = 100; - let at = new Vec2(width / 2, width / 2); + let size = 0.6; + let distance_away = 3; - let old_fov = camera.fov; - camera.fov = Math.PI / 4; + let v = new Vec3(0.0, distance_away, 0.0).rotate_about_x(camera.pitch).rotate_about_z(camera.yaw); + 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_at = camera.camera_to_world(new Vec3(0, distance, 0), false); + let gizmo_screen = cam.project(gizmo_world, true); - for (let direction of DIRECTIONS) { - let gizmo_up = gizmo_at.add(direction.dir); - let up_cam = camera.project(gizmo_up, false); - let xy = up_cam.camera_space.mul(distance); - xy = xy.mul(size / 3); + let directions = [ + { dir: gizmo_screen.xhat_screen, color: "rgb(0, 0, 255)" }, + { dir: gizmo_screen.yhat_screen, color: "rgb(0, 180, 0)" }, + { dir: gizmo_screen.zhat_screen, color: "rgb(255, 0, 0)" }, + ]; + for (let direction of directions) { ctx.beginPath(); ctx.strokeStyle = direction.color; ctx.lineWidth = 4; - ctx.moveTo(at.x, at.y); - ctx.lineTo(at.x + xy.x, at.y - xy.y); + let start = gizmo_screen.screen_space; + 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(); }