From d084c6097b654259e0c27df9add8a07d12795f83 Mon Sep 17 00:00:00 2001 From: loovjo Date: Thu, 20 Jul 2023 02:31:34 +0200 Subject: [PATCH] initial commit; basic 3d engine --- .gitignore | 1 + main.html | 23 ++++ xen.ts | 306 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 .gitignore create mode 100644 main.html create mode 100644 xen.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6c7c28 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/main.html b/main.html new file mode 100644 index 0000000..a191c99 --- /dev/null +++ b/main.html @@ -0,0 +1,23 @@ + + + + coral.shoes + + + + + +
+ +
+ + + diff --git a/xen.ts b/xen.ts new file mode 100644 index 0000000..43c764b --- /dev/null +++ b/xen.ts @@ -0,0 +1,306 @@ +class Vec2 { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + add(v: Vec2): Vec2 { + return new Vec2(this.x + v.x, this.y + v.y); + } + + sub(v: Vec2): Vec2 { + return new Vec2(this.x - v.x, this.y - v.y); + } + + mul(multiplicand: number): Vec2 { + return new Vec2(this.x * multiplicand, this.y * multiplicand); + } + + div(divisor: number): Vec2 { + return new Vec2(this.x / divisor, this.y / divisor); + } + + dot(v: Vec2): number { + return this.x * v.x + this.y * v.y; + } + + length(): number { + return Math.sqrt(this.dot(this)); + } + + normalize(): Vec2 { + return this.div(this.length()); + } +} + +class Vec3 { + // x, right, y forward, z up + x: number; + y: number; + z: number; + constructor(x: number, y: number, z: number) { + this.x = x; + this.y = y; + this.z = z; + } + + add(v: Vec3): Vec3 { + return new Vec3(this.x + v.x, this.y + v.y, this.z + v.z); + } + + sub(v: Vec3): Vec3 { + return new Vec3(this.x - v.x, this.y - v.y, this.z - v.z); + } + + mul(multiplicand: number): Vec3 { + return new Vec3(this.x * multiplicand, this.y * multiplicand, this.y * multiplicand); + } + + div(divisor: number): Vec3 { + return new Vec3(this.x / divisor, this.y / divisor, this.y / divisor); + } + + dot(v: Vec3): number { + return this.x * v.x + this.y * v.y; + } + + length(): number { + return Math.sqrt(this.dot(this)); + } + + normalize(): Vec2 { + return this.div(this.length()); + } + + // maps angle 0 to angle phi + rotate_about_z(phi: number): Vec3 { + let sp = Math.sin(phi); + let cp = Math.cos(phi); + return new Vec3( + cp * this.x - sp * this.y, + sp * this.x + cp * this.y, + this.z + ); + } + + // maps angle 0 to angle theta + rotate_about_x(theta: number): Vec3 { + let st = Math.sin(theta); + let ct = Math.cos(theta); + return new Vec3( + this.x, + ct * this.y - st * this.z, + st * this.y + ct * this.z, + ); + } +} + +interface ProjectedPoint { + camera_space: Vec2, // [-1, 1]x[-1, 1] + distance: number, + in_view: boolean, // outside view? + behind: boolean, +} + +class CameraProjection { + origin: Vec3; + yaw: number; + pitch: number; + fov: number; + + constructor(origin: Vec3, yaw: number, pitch: number, fov: number) { + this.origin = origin; + this.yaw = yaw; + this.pitch = pitch; + this.fov = fov; + } + + world_to_camera(point: Vec3, respect_origin: boolean): Vec3 { + if (respect_origin) { + point = point.sub(this.origin); + } + return point.rotate_about_z(-this.yaw).rotate_about_x(-this.pitch); + } + + camera_to_world(point: Vec3, respect_origin: boolean): Vec3 { + point = point.rotate_about_x(this.pitch).rotate_about_z(this.yaw); + if (respect_origin) { + point = point.add(this.origin); + } + return point; + } + + project(point: Vec3, respect_origin: boolean): ProjectedPoint { + 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 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); + return { + camera_space: camera_space, + distance: distance, + in_view: in_view, + behind: behind, + }; + } +} + +type RGB = {r: number, g: number, b: number}; + +interface Drawable { + at: Vec3; + render(at: ProjectedPoint, camera: CameraProjection, ctx: CanvasRenderingContext2D): boolean; +} + +class Ball { + at: Vec3; + color: RGB; + constructor(at: Vec3, color: RGB) { + this.at = at; + this.color = color; + } + render(at: ProjectedPoint, cam: CameraProjection, ctx: CanvasRenderingContext2D): boolean { + 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.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; + +canvas.width = width; +canvas.height = height; +canvas.style = `width: ${width}px; 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 pressing = new Set(); + +document.addEventListener("keydown", e => { + pressing.add(e.code); + if (e.code === "Space") { + should_draw_gizmo = !should_draw_gizmo; + } +}); + +document.addEventListener("keyup", e => { + pressing.delete(e.code); +}); + +function step() { + let delta = 1 / FPS; + if (pressing.has("ShiftLeft")) { + delta *= 0.2; + } + if (pressing.has("AltLeft")) { + delta *= 5; + } + let position_delta = new Vec3(0, 0, 0); + if (pressing.has("KeyW")) { + position_delta.y += delta; + } + if (pressing.has("KeyA")) { + position_delta.x -= delta; + } + if (pressing.has("KeyS")) { + position_delta.y -= delta; + } + if (pressing.has("KeyD")) { + position_delta.x += delta; + } + if (pressing.has("KeyQ")) { + position_delta.z += delta; + } + if (pressing.has("KeyZ")) { + position_delta.z -= delta; + } + camera.origin = camera.origin.add(position_delta.rotate_about_z(camera.yaw)); + + if (pressing.has("ArrowRight")) { + camera.yaw -= delta; + } + if (pressing.has("ArrowLeft")) { + camera.yaw += delta; + } + if (pressing.has("ArrowUp")) { + camera.pitch += delta; + } + if (pressing.has("ArrowDown")) { + camera.pitch -= delta; + } + redraw(camera); + if (should_draw_gizmo) { + draw_gizmo(ctx, camera); + } +} + +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 redraw(camera: CameraProjection) { + ctx.clearRect(0, 0, width, height); + + let rendered = objects + .map(o => {return {object: o, at: camera.project(o.at, true )}}) + .filter(r => r.at.in_view) + .sort((a, b) => -(a.at.distance - b.at.distance)); + + for (let x of rendered) { + x.object.render(x.at, camera, ctx); + } +} + +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 old_fov = camera.fov; + camera.fov = Math.PI / 4; + + let distance = 100; + let gizmo_at = camera.camera_to_world(new Vec3(0, distance, 0), false); + + 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); + + 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); + ctx.stroke(); + } + + camera.fov = old_fov; +} + +setInterval(step, 1000 / FPS);