initial commit; basic 3d engine
This commit is contained in:
commit
d084c6097b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.js
|
23
main.html
Normal file
23
main.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>coral.shoes</title>
|
||||
<meta name="iloinlxlvi" content="https://coral.shoes/iloinlxlvi.json">
|
||||
<link rel="stylesheet" href="resources/main.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
main, canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<canvas id="frame"></canvas>
|
||||
</main>
|
||||
</body>
|
||||
<script src="xen.js"></script>
|
||||
</html>
|
306
xen.ts
Normal file
306
xen.ts
Normal file
|
@ -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<Drawable> = [];
|
||||
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);
|
Loading…
Reference in New Issue
Block a user