top of page
Search
cedarcantab

2D Raycasting using Phaser 3, Part 1

Updated: May 5, 2023

This post is about my exploration into Phaser.Geom.Intersect methods, which eventually led to the raycasting demo below (and more, to be covered in part 2).



Phaser.Geom.Intersects


Under Namespaces, there is something called Phaser.Geom.Intersects, under which there are lots of very interesting sounding methods. While browsing through the documentation, I came across GetRaysFromPointToPolygon, for which the following explanation is given.


Projects rays out from the given point to each line segment of the polygons.

If the rays intersect with the polygons, the points of intersection are returned in an array.

If no intersections are found, the returned array will be empty.

Each Vector4 intersection result has the following properties:

The x and y components contain the point of the intersection. The z component contains the angle of intersection. The w component contains the index of the polygon, in the given array, that triggered the intersection.


The explanation is clear, so I set about creating the example below (inspiration from a fascinating site by Nicky Case called SIGHT & LIGHT, here) .


GeyRaysFromPointToPolygon


The method is easy to use. Literally, you create the obstacles as Geom.Polygons, stick them into an array, create a Geom.Point object that represents the "origin" from which to shoot rays, then call the method to get the intersection points, which are returned in an array.


The rturned array contains the worldXY coordinates of the intersections. Once you have this information, simply draw pink lines to those intersection points and (since you are given an array of points), Phaser's fillPoinst method to draw the "light"! It really is that simple!



The CODEPEN of this code is accessible below.




A point to watch in creating the polygons

You do need to be aware of one thing, and that is, when creating the obstacle polygons, it seems to work best if you "close" the polygon by insering the last point to overlap the first point, even though when drawing polygons, Phaser's method can automatically "close" the drawn image. For example, for the border, I used the code below.


this.obstacles.push(new Phaser.Geom.Polygon([0,0,SCREEN_WIDTH,0,SCREEN_WIDTH,SCREEN_HEIGHT,0,SCREEN_HEIGHT]));

But how does this work?


The method basically loops through each polygon in turn, and for each polygon, it looks for the intersection point between the ray projecting out of the reference point to each of the vertices of the polygons. In addition to projecting rays to each of the vertices of every polygon, 2 additional rays slight to the left and right of the vertex are projected, so that you don't have great big gaps - it is easier to read the code, which is reproduced below.


for (var p = 0; p < points.length; p++)  {
            var angle = Math.atan2(points[p].y - y, points[p].x - x);
            if (angles.indexOf(angle) === -1)            {
                //  +- 0.00001 rads to catch lines behind segment corners
                CheckIntersects(angle, x, y, polygons, intersects);
                CheckIntersects(angle - 0.00001, x, y, polygons, intersects);
                CheckIntersects(angle + 0.00001, x, y, polygons, intersects);
                angles.push(angle);
            }

The method above calls the function called CheckIntersects (I don't think this is a publically available method) which creates a "line" from the origin set at the angle in which the ray is cast, and calls the GetLineToPolygon method (this is available as a Phaser.Geom.Intersects method.


function CheckIntersects (angle, x, y, polygons, intersects)
{
    var dx = Math.cos(angle);
    var dy = Math.sin(angle);

    segment.setTo(x, y, x + dx, y + dy);

    var closestIntersect = GetLineToPolygon(segment, polygons);

    if (closestIntersect)
    {
        intersects.push(new Vector4(closestIntersect.x, closestIntersect.y, angle, closestIntersect.w));
    }
}

So let's look in more detail at this method, which is at the heart of the GeyRaysFromPointToPolygon method.


Phaser.Geom.Intersects.GetLineToPolygon(line, polygons [, out])


Official documentation explanation is as follows.


Checks for the closest point of intersection between a line segment and an array of polygons.

If no intersection is found, this function returns null.

If intersection was found, a Vector4 is returned with the following properties:

The x and y components contain the point of the intersection. The z component contains the closest distance. The w component contains the index of the polygon, in the given array, that triggered the intersection.


So it looks like this is essentially the single line version of the getRaysFromPointToPolygon method, except this method returns the distance information in the z-component, whereas the getRays method returns the angle of intersection.


And indeed, you can use this method to cast one ray out from a point.


The CodePen for the above is available below.





Phaser.Geom.Intersection.GetLineToPoints(line, points [, out])

But how does this method work??? Well it actually loops through all the polygons, and calls another method GetLineToPoints! Of this method, the official documentation explains "Checks for the closest point of intersection between a line segment and an array of points, where each pair of points are converted to line segments for the intersection tests.".


   var prev = points[0];
    for (var i = 1; i < points.length; i++)    {
        var current = points[i];
        segment.setTo(prev.x, prev.y, current.x, current.y);
        prev = current;
        if (GetLineToLine(line, segment, tempIntersect)) {
            if (!closestIntersect || tempIntersect.z < out.z) {
                out.copy(tempIntersect);
                closestIntersect = true;
            }
        }
    }
    return (closestIntersect) ? out : null;

The code, as shown above loops through all the points creating line objects with consecutive pairs, calling the another method called Phaser.Geom.Intersection.GetLineToLine which returns the intersection point between 2 lines. Each time the returned point is returned, if the distance is smaller than the previous distance, out variable is set to the closer distance. Hence, at the end of the loop, the closest intersection point is returned.


GetLineToLine(line1, line2 [, out])

And this method really is the end of the road. This method does not call any other methods!


The documentation states:


It checks for intersection between the two line segments and returns the intersection point as a Vector3, or null if the lines are parallel, or do not intersect. The z property of the Vector3 contains the intersection distance, which can be used to find the closest intersecting point from a group of line segments.


The code for this method is as follows:


varGetLineToLine=function(line1,line2,out)
    .....
var denom = dy2 * dx1 - dx2 * dy1;
    if (dx1 === 0 || denom === 0)    {
        return false;
    }
    var T2 = (dx1 * (y3 - y1) + dy1 * (x1 - x3)) / (dx2 * dy1 - dy2 * dx1);
    var T1 = (x3 + dx2 * T2 - x1) / dx1;
    //  Intersects?
    if (T1 < 0 || T2 < 0 || T2 > 1)    {
        return null;
    }
    if (out === undefined)    {
        out = new Vector3();
    }
    return out.set(
        x1 + dx1 * T1,
        y1 + dy1 * T1,
        T1
    );

The above code looks complicated but is in fact easy to understand if you imagine that it is treating the line segments as vectors. More specifically,


line1 = point a (x1,y1) -> point b (x2,y2), or (x1,y1) + {(x2,y2) - (x1,y1)} * T1

line2 = point a (x3.y3) -> point b (x4,y4), or (x3,y3) + {(x4,y4) - (x3,y3)} * T2


You solve for T1 and T2 which results in the same point, ie intersecting point. You can clearly see this in the demonstration below, where the red line segment does not actually intersect with the blue line segment but the two are assessed as intersecting with the intersecting point lying on the blue segment on the "extension" of the red line.



One should note the red highlighted code, which checks whether the lines actually intersect at all. Specifically, it is checking to see if T2 is within 0 and 1 (ie within the "segment" defined by the two ends of line2. However, T1 is only checked to see if is >0. In other words, any "intersecting point" between line2 and any point lining along the "extension" of line1 beyond its end point will be counted as an intersection.


T1 is returned in the z property of the vector. You need to remember that this is a value between 0 and 1 (in fact, T1 can be > 1, if line1 does not actually intersect, like in the below case), and not the distance in terms of number of pixels. As far as I can tell, if you wanted the distance from the beginning of line1 to the intersection point in terms of pixels, you could:


(i) multiply the length of line 1 by the t-value

(ii) pass a "unitised" (ie length of 1) line as paramter line1 to the method

(iii) simply to calculate the distance between the returned intersection point and the start of line1, using the usual distance between two points formula.


(i) and (ii) both requires the calculation of length of line1, which requires square rooting. (iii) requires calculation of distance of 2 points, which also requires square rooting.


The code to graphically demonstrate Phaser's Line to Line intersection method is accessible in Codepen below.





Using ray casting to draw a "3D" scene from 2D map


Which finally brings me back to the code to render a "3D" scene from a 2D map using ray casting, as shown in the picture at the beginning of this post. Suffice to say, using Phaser 3's Phaser.Geom.Intersects.GetLineToPolygon method made coding this very easy.


I will not explaint the algorithm here, since the video here explains the ideas way better than I ever could in text. The Codepen is available below, for your perusal and comment.




Useful references




120 views0 comments

Recent Posts

See All

p2 naive broadphase

var Broadphase = require('../collision/Broadphase'); module.exports = NaiveBroadphase; /** * Naive broadphase implementation. Does N^2...

sopiro motor constranit

import { Matrix2, Vector2 } from "./math.js"; import { RigidBody } from "./rigidbody.js"; import { Settings } from "./settings.js";...

Comments


記事: Blog2_Post
bottom of page