Raycasting 2D

The Idea

Raycasting is from a mathematical perspective nothing but calculating line-line intersections.
One line represents the outline of an obstacle, e.g. a wall or an enemy. The other line can be a ray of light. Multiple rays of light sum up to a view of e.g. the hero in a computer game.
Asuming the field of view of a hero is 180 degree - to be more precise from -90 deg to +90 deg, we will send out a ray of light every 1 deg and check if the ray of light intersects with any obstacle. If multiple obstacles exist in the direction the ray travels we have to calculate the closest obstacle, because only this will be seen. All others will be covered by the closest.

The Walls

To build this 2D raycasting script, I started creating a class Border, which is a line defined by two points
The constructor of class Border gets two coordinates, each consisting of an x and a y element.

class Border {
 constructor(x1, y1, x2, y2) {
  this.a = createVector(x1, y1);
  this.b = createVector(x2, y2);
 }
}

All this class needs in addition is a draw method to draw a line from one point to the other

draw() {
 stroke(255);
 line(this.a.x, this.a.y, this.b.x, this.b.y);
}

The Ray

The most important element in this script is the Ray. Our Ray needs a position to start from and an angle giving the direction to travel. The position will be given by a p5.Vector. As vectors are defined by an angle and a length, we can also use a p5.Vector for the position of the ray. Hence the constructor of Ray gets two parameters, a vector and an angle, and creates two p5.Vector out of them. To make it more visible I set the length of the angle vector to 10.

class Ray {
 constructor(position, angle) {
  this.pos = position;
  this.dir = p5.Vector.fromAngle(angle, 10);
 }
}

Next, we need a draw method, to draw the base of the ray. This means, this is not the final ray we draw, just a small stump, to see if we are on the right track.

draw() {
 push();
 translate(this.pos.x, this.pos.y);
 line(0, 0, this.dir.x, this.dir.y);
 pop();
}

Computing the line-line intersection

The next step is the most important step in the script. We will compute the line-line intersection of the ray and the wall. This is based on wikipedia. Alternativly you can use the cross product approach discussed on stackoverflow.
The definition of the 8 constants at the beginning of the script is not necessary. I just did it to reduce type-o risks.
If the denominator is equal to zero, parameter t is smaller than zero, parameter t is greater than 1 or parameter u is smaller than zero, the ray and the wall do not intersect. If both lines intersect, the method will return the intersect point as a p5.Vector.

cast(wall) {
 const x1 = wall.a.x;
 const y1 = wall.a.y;
 const x2 = wall.b.x;
 const y2 = wall.b.y;

 const x3 = this.pos.x;
 const y3 = this.pos.y;
 const x4 = this.pos.x + this.dir.x;
 const y4 = this.pos.y + this.dir.y;
 let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);

 if (denominator == 0) {
  return;
 }

 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
 let u = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;

 if (t > 0 && (t < 1) & (u > 0)) {
  let x = x1 + t * (x2 - x1);
  let y = y1 + t * (y2 - y1);
  return createVector(x, y);
 }

 return;
}

A Patricle of Rays

In class Particle we accumulate a bunch of Rays. In my case I used 360 Rays - each degree a Ray - to get a light bulb like apearance. The constructor of the class gets a position as p5.Vector as input and populates an array with Rays.

constructor(position) {
 this.pos = position;
 this.rays = [];
 this.angle = 0;

 for (let i = 0; i < 360; i += 1) {
  this.rays.push(new Ray(this.pos, radians(i)));
 }
}

Of course also class Particle needs a draw method, which calls the draw method of each Ray.

draw() {
 for (const ray of this.rays) {
  ray.draw();
 }
}

And a update method to be able to change the posion of the patricle.

update(x, y) {
 this.pos.set(x, y);
}

The most interesting method is the lookAt method, which is used to compute the intersections of each ray with the walls. For each ray and wall the closest wall is stored in variable closest. To detect if a wall is closer than the last, variable record holds the distance to the closest wall. If all walls are checked a line from the Ray's starting point to the wall is drawn.

lookAt(walls) {
 for (const ray of this.rays) {
  let closest = null;
  let record = Infinity;

  for (const wall of walls) {
   let intersect = ray.cast(wall);
   if (intersect) {
    let distance = p5.Vector.dist(this.pos, intersect);
    if (distance < record) {
     record = distance;
     closest = intersect;
    }
   }
  }
  if (closest) {
   stroke(255, 50);
   line(this.pos.x, this.pos.y, closest.x, closest.y);
  }
 }
}

The Final Sketch

We need two globals variables to store an instance of class Particle and all our Walls

let walls = [];
let particle;

In the setup function we create a canvas, five walls on random positions and a wall on each outer edge of the canvas. Finally, we create an instance of Particle on any position.

function setup() {
 myCanvas = createCanvas(600, 400);

 for (let i = 0; i < 5; i++) {
  walls.push(new Border(random(500), random(400),
   random(500), random(400)));
 }
 walls.push(new Border(0, 0, width, 0));
 walls.push(new Border(width, 0, width, 400));
 walls.push(new Border(width, 400, 0, 400));
 walls.push(new Border(0, height, 0, 0));
 particle = new Particle(createVector(100, 200));
}

In the draw function we draw all the walls and update Particle's postion according to the mouse position. Then we draw the Particle and compute all the Rays.

function draw() {
 background(color(0, 50, 100));

 for (const wall of walls) {
  wall.draw();
 }

 if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < 400) {
  particle.update(mouseX, mouseY);
 }

 particle.draw();
 particle.lookAt(walls);
}

Last edit: 2023-01-23