piqnt / planck.js

2D JavaScript Physics Engine
http://piqnt.com/planck.js/
MIT License
4.87k stars 236 forks source link

Raycasts phasing through walls #270

Closed codingMASTER398 closed 7 months ago

codingMASTER398 commented 7 months ago

Hello! p5play user here. Although ray-casting isn't officially supported in p5play, there's a simple workaround to access the native APIs.

I've attached a video of the error occurring. Screencast from 2024-01-29 16-38-38.webm

I'm doing asynchronous raycasts like this:

function raycast(pos, direction){
    return new Promise((res)=>{
        let rays = [], start = planck.Vec2(pos.x, pos.y), end = planck.Vec2(
            Math.round(pos.x + (10  * Math.cos(direction * Math.PI / 180))),
            Math.round(pos.y + (10  * Math.sin(direction * Math.PI / 180)))
        )

        world.rayCast(start, end, (c, c2)=>{ // Raycast is here
            c.intersection = c2
            rays.push(c)
        })

        setTimeout(()=>{
            res(rays)
        }, 10)
    })
}

Taking in a position of the origin, then doing some maths to figure out a position in the direction I want, casting & waiting 10ms for all the responses.

I then sort them:

rays = rays.sort((a,b)=>{
    return dist(a.intersection.x, a.intersection.y, this.obj.x, this.obj.y)
        - dist(b.intersection.x, b.intersection.y, this.obj.x, this.obj.y)
})

and I assume the first element of that array is the closest raycast, however in the video you can see that is not always the case, mostly between certain rotations?

I'm fairly new to this so any advice will be extremely helpful. Sorry I can't provide an in-depth explanation.

codingMASTER398 commented 7 months ago

I inexplicably solved this using the "RayCastClosest" example at https://piqnt.com/planck.js/RayCast . I just imported the function, and everything seemed to... magically work? No idea what wizardry it does. More helpful documentation on this would be nice.

shakiba commented 7 months ago

What function did you import that solved the problem?

quinton-ashley commented 5 months ago

@shakiba In the docs, the use of the term "callback" in WorldRayCastCallback is a bit misleading because planck's world.rayCast is not an async function. "callback" in JavaScript refers to an input function that runs after the called function finishes, almost always because the caller function is async. I think the input function to world.rayCast could more accurately be called a filter.

https://github.com/piqnt/planck.js/blob/master/docs/api/classes/world.md#raycast

Also I think since codingMASTER was only interested in finding the closest callback so he found the solution to his problem in your example's RayCastClosest function.

@codingMASTER398 This may have been added by Shakiba later but the example now notes "do not assume that fixtures are reported in order. However, by clipping, we can always get the closest fixture."

Why is that? It's because the ray cast is not performed in the straight forward way one may expect, by testing points along the ray cast line, which is called Ray Marching.

Ray casting in Box2D is done by looking at nearby fixtures in the Dynamic AABB Tree, a specialized data structure that stores all the fixture's AABBs. The visually closest nodes to the fixture at the starting point may be stored further apart in the dynamic tree. Since planck's raycasting actually works by looping through fixtures in the tree, that's why fixtures may not be reported in the order a ray would hit them. Complicated stuff aye?

This also means that unlike with ray marching, we can't cancel a planck raycast just because for example it hits a wall, because the wall might be behind a target object.

quinton-ashley commented 5 months ago

In the filter function, returning:

-1 stops the ray casting greater than 0 limits the ray by a fraction of the distance to the second point 1 maintains the full length of the ray cast

shakiba commented 5 months ago

@quinton-ashley thanks for your comments and suggestions.

To clarify, the return value of the callback is used to update the initial maxFraction in the raycast input. So 0 would effectively stop the raycast, and negative is ignored as invalid.

Also, I think the parameter is named correctly according to MDN:

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.

Examples of synchronous callbacks include the callbacks passed to Array.prototype.map(), Array.prototype.forEach(), etc. Examples of asynchronous callbacks include the callbacks passed to setTimeout() and Promise.prototype.then().

quinton-ashley commented 5 months ago

@shakiba you are right! hmm I didn't know those kinds of functions were also called callbacks.