How to do a curved raycast?

I can draw a line with pc.Curve, but can i use the same to create a ray?

I imagine you use a loop to get the key/segments of the curve and you draw a line for each?

Not sure what you are trying to achieve but I imagine you can do the same, a ray/raycast for each line of the curve.

I draw a throw-line between the player and the mouse position using this method: https://playcanvas.com/project/438429/overview/sample-camera-on-a-path

But i want to change the end-point of the line if there is an obstacle in this path. To do that i want to cast a ray with the same path.

Ah right, so you can’t cast a ray using a curve. But you can do a loop and start casting rays for each curve segment, much like you do to draw lines.

If any of the individual rays hits an obstacle then you have what you want.

Okay, how we gonna do that? :see_no_evil:

Can you post the code you use to draw your curve using lines?

I try to past my code but because i only can use my mobile right now it does not work.

I use almost the same script as the example project only i use it in the update.

I set the throwEnd to the mouse position at another script but you can add it to this script. The sensorFront part is this is script was a trail and can be removed.

var Throw = pc.createScript('throw');

Throw.attributes.add('color', { type: 'rgba' });
Throw.attributes.add('divisions', { type: 'number', default: 25, min: 3, max: 50 });


var throwInRange;
var throwEnd;

// initialize code called once per entity
Throw.prototype.initialize = function() {
    this.initializeLine();
};

// update code called every frame
Throw.prototype.update = function(dt) {
    this.updateLine(dt);
};

Throw.prototype.initializeLine = function() {
    this.point1 = this.entity.findByName("Point1");
    this.point2 = this.entity.findByName("Point2");
    this.point3 = this.entity.findByName("Point3");
    throwEnd = this.app.root.findByName("ThrowEnd");
};


Throw.prototype.updateLine = function (dt) {
    var children = this.entity.children;
    
    if (children.length !== 3) {
        console.error('You need to create three child entities of the Bezier entity that are control points.');
    }
    
    var middle = this.point3.localPosition.z / 2;
    this.point2.setLocalPosition(new pc.Vec3(this.point2.getLocalPosition().x, this.point2.getLocalPosition().y, middle));
    
    var p1 = children[0].getPosition();
    var p2 = children[1].getPosition();
    var p3 = children[2].getPosition();
    
    this.curve = new Bezier(p1.x, p1.y, p1.z, p2.x, p2.y+1.25, p2.z, p3.x, p3.y, p3.z);
    
    this.p1 = new pc.Vec3();
    this.p2 = new pc.Vec3()    ;

    this.white = new pc.Color(1, 1, 1, 0.1);
    
    var divisions = Math.floor(this.divisions);
    
    // Render the curve itself
    var lut = this.curve.getLUT(divisions);
    for (var i = 0; i < lut.length-1; i++) {
        this.p1.x = lut[i].x;
        this.p1.y = lut[i].y;
        this.p1.z = lut[i].z;
        this.p2.x = lut[i+1].x;
        this.p2.y = lut[i+1].y;
        this.p2.z = lut[i+1].z;
        //this.app.renderLine(this.p1, this.p2, this.color);

        if (playerIsUsingStone || playerIsUsingGrenade) {
            if (playerIsGrounded && !playerIsClimbing) {
                if (this.getDistance(this.entity.getPosition(), this.point3.getPosition()) < 1.5 || this.getDistance(this.point1.getPosition(), this.point3.getPosition()) > 9) {
                    this.app.renderLine(this.p1, this.p2, colorRed);
                    this.entity.findByName("Circle").sprite.color = colorRed;
                    this.entity.findByName("Circle").sprite.opacity = 0.1;
                    throwInRange = false;
                }

                else {
                    this.entity.enabled = true;
                    this.app.renderLine(this.p1, this.p2, colorGreen);
                    this.entity.findByName("Circle").sprite.color = colorGreen;
                    this.entity.findByName("Circle").sprite.opacity = 0.1;
                    throwInRange = true;
                    
                    this.sensorFront = this.app.systems.rigidbody.raycastFirst(this.p1, this.p2);
                    
                    if (this.sensorFront)
                        {
                            //throwEnd.setPosition(this.sensorFront.point);
                                                
                        }
                    else {
                   //throwEnd.setPosition(this.point3.getPosition);
                    }
                    

                }
            }
            
            else {
                this.entity.findByName("Circle").sprite.opacity = 0;
            }
        }
    }
};

// CALCULATE DISTANCE
Throw.prototype.getDistance = function (pos1, pos2) {
    var x = pos1.x - pos2.x;
    var y = pos1.y - pos2.y;
    var z = pos1.z - pos2.z;

    var temp = new pc.Vec3(x, y, z);
    return temp.length ();
};

Then some pseudo code based on the camera example naming:

// curves
// this.px
// this.py
// this.pz 

// on init
this.step = 0.1;
this.from = new pc.Vec3();
this.to = new pc.Vec3();

// when checking if your throw curve is valid or there are obstacles on path
for (var i = 0; i < 1; i+= this.step) {
   var current = i;
   var next = i + this.step;

   this.from.set(this.px.value(current), this.py.value(current), this.pz.value(current));
   this.to.set(this.px.value(next), this.py.value(next), this.pz.value(next));

   // Do your usual raycast using from/to position, if obstacle found break
}

Okay thanks! I’ll give it a try when I get home. I’ll keep you informed.

1 Like

I accidentally sent you the example project that I use for the throwable object (that will follow the curved line). For rendering the curved line itself i have use this example project: https://playcanvas.com/project/500630/overview/bezier

Can you add the code for checking if there is an obstacle in the way of the curved line in my code below? (So if there is no obstacle in the way, then the last point position is the mouse point position, otherwise the last point position is the raycast point that you add with your code).

var Throw = pc.createScript('throw');

Throw.attributes.add('color', { type: 'rgba' });
Throw.attributes.add('divisions', { type: 'number', default: 25, min: 3, max: 50 });

var throwInRange;

// initialize code called once per entity
Throw.prototype.initialize = function() {
    this.initializeLine();
};

// update code called every frame
Throw.prototype.update = function(dt) {
    this.updateLine(dt);
};

Throw.prototype.initializeLine = function() {
    this.point1 = this.entity.findByName("Point1");
    this.point2 = this.entity.findByName("Point2");
    this.point3 = this.entity.findByName("Point3");
};


Throw.prototype.updateLine = function (dt) {
    var children = this.entity.children;
    
    if (children.length !== 3) {
        console.error('You need to create three child entities of the Bezier entity that are control points.');
    }
    
    // Set the middle point between the first and last point
    var middle = this.point3.localPosition.z / 2;
    this.point2.setLocalPosition(new pc.Vec3(this.point2.getLocalPosition().x, this.point2.getLocalPosition().y, middle));
    
    var p1 = children[0].getPosition();
    var p2 = children[1].getPosition();
    var p3 = children[2].getPosition();
    
    this.curve = new Bezier(p1.x, p1.y, p1.z, p2.x, p2.y+1.25, p2.z, p3.x, p3.y, p3.z);
    
    this.p1 = new pc.Vec3();
    this.p2 = new pc.Vec3();
    
    var divisions = Math.floor(this.divisions);
    
    // Render the curve itself
    var lut = this.curve.getLUT(divisions);
    for (var i = 0; i < lut.length-1; i++) {
        this.p1.x = lut[i].x;
        this.p1.y = lut[i].y;
        this.p1.z = lut[i].z;
        this.p2.x = lut[i+1].x;
        this.p2.y = lut[i+1].y;
        this.p2.z = lut[i+1].z;

        // Show only if player is using a throwable object
        if (playerIsUsingStone || playerIsUsingGrenade) {
            
            // Check if player is grounded
            if (playerIsGrounded && !playerIsClimbing) {
                
                // Set throw end to mouse position if there is no obstacle
                this.entity.findByName("Point3").setPosition(new pc.Vec3(mousePoint.getPosition().x, player.getPosition().y - 0.5, mousePoint.getPosition().z));
            
                
                // If throw end is out of range
                if (this.entity.getPosition().distance(this.point3.getPosition()) < 1.5 || this.point1.getPosition().distance(this.point3.getPosition()) > 9) {
                    throwInRange = false;
                    
                    // Render the throw line RED
                    this.app.renderLine(this.p1, this.p2, colorRed);
                    
                    // Set the effect circle RED
                    this.entity.findByName("Circle").sprite.color = colorRed;
                    this.entity.findByName("Circle").sprite.opacity = 0.1;
                }

                // If ready to throw
                else {
                    throwInRange = true;
                    
                    // Render the trow line GREEN
                    this.app.renderLine(this.p1, this.p2, colorGreen);
                    
                    // Set the effect circle GREEN
                    this.entity.findByName("Circle").sprite.color = colorGreen;
                    this.entity.findByName("Circle").sprite.opacity = 0.1;
                    
                }
            }
        }
    }
};

Sorry I can’t really do this out of context.

But from what I see from your code you already have a loop to draw each line segment.

Your from and to points would be:

   this.from.set(this.p1.x, this.p1.y, this.p1.z);
   this.to.set(this.p2.x, this.p2.y, this.p2.z);

Now you can use those two vectors to do a raycast and check for each part of your curve if there is an obstacle.

1 Like

I finally succeeded! Thank you very much!

1 Like

As you can see in the video below, it does not yet work optimally. Any idea how I can optimize my code? (The three dots are the three point that control the line).

var Throw = pc.createScript('throw');

// initialize code called once per entity
Throw.prototype.initialize = function() {
    this.divisions = 25;
    this.pathRoot = this.entity;
    this.point1 = this.entity.findByName("Point1");
    this.point2 = this.entity.findByName("Point2");
    this.point3 = this.entity.findByName("Point3"); 
};

// update code called every frame
Throw.prototype.update = function(dt) {
    // Set the middle point between the first and last point
    var middle = this.point3.localPosition.z / 2;
    this.point2.setLocalPosition(new pc.Vec3(this.point2.getLocalPosition().x, this.point2.getLocalPosition().y, middle));
    
    // Set the last point to the mouse position
    this.point3.setPosition(mousePoint.getPosition());
    
    var children = this.entity.children;
    
    if (children.length !== 3) {
        console.error('You need to create three child entities of the Bezier entity that are control points.');
    }
    
    var p1 = children[0].getPosition(); // this.point1
    var p2 = children[1].getPosition(); // this.point2
    var p3 = children[2].getPosition(); // this.point3
    
    this.curve = new Bezier(p1.x, p1.y, p1.z, p2.x, p2.y+1.3, p2.z, p3.x, p3.y, p3.z);
    
    this.from = new pc.Vec3();
    this.to = new pc.Vec3();
    
    var divisions = Math.floor(this.divisions);
    
    // Render the curve itself
    var lut = this.curve.getLUT(divisions);
    for (var i = 0; i < lut.length-1; i++) {
        this.from.x = lut[i].x;
        this.from.y = lut[i].y;
        this.from.z = lut[i].z;
        this.to.x = lut[i+1].x;
        this.to.y = lut[i+1].y;
        this.to.z = lut[i+1].z;
        
        // Render the line
        this.app.renderLine(this.from, this.to, colorRed);
        
        // Set last point if there is an obstacle between
        throwSensor = this.app.systems.rigidbody.raycastFirst(this.from, this.to);

        if (throwSensor) {
            this.point3.setPosition(throwSensor.point);
            return;       
        }
    }
};

At a guess, it looks like you don’t stop raycasting when the curve ‘hits’ something in the environment.

I can’t stop the raycast because it has to check every mouse movement if the throw line has a free path. If not, the throw line will be adjusted. Only when the player actually throws I can stop the raycast.

Maybe it’s because the raycast consists of steps? Because when one of the steps hit something, all the previous steps don’t hit anything, so the raycast is true and false at the same time.

So how can I make sure that the throwSensor stays true if one of the raycast steps hit something?

Before you start going through the steps for the ray casts, set throwSensor to false and only set to to true if the cast hits something. ie:

throwSensor = false;
for (var i = 0; i < steps; ++i) {
  if (hasStepHitSomething) {
    throwSensor = true;
  }
}

if (throwSensor) {
  // Do stuff
}

Everything I try works on small objects. If I make the objects (and their colliders) larger, the raycast often goes through. In Unity there was a function to use spherecast. I think that had been a solution.

Maybe someone with a lot of experience would like to make a curved raycast that looks at the mouse position. Set an object to the mouse position when there is no obstacle in the way. Otherwise set the object to the position of the hit point of the raycast.

When I add a slightly smaller child object to the wall objects with also a collider and rigidbody, then everything works fine. That means that the raycast simply misses the single colliders.

I think it’s not the best solution to add extra child objects, but if nobody can help me to improve the raycast, I will have to do it that way.