Welcome to the Second Life Forums Archive

These forums are CLOSED. Please visit the new forums HERE

Ray intersection and hitting a target

Macie Forzane
Registered User
Join date: 4 Jan 2009
Posts: 3
01-04-2009 04:01
Hi,

******** Direction vector used in the Popgun ******

I am trying to modify the Popgun that can be found in our inventory library so that it can detect whether a target would be hit without actually firing bullets. However I'm completely stuck on the direction vector used in the Popgun.

The algorithm I've implemented is a simple Ray-Sphere Test from a book on Games Programming. It just tests whether a ray from the gun would hit or miss the target sphere.

The algorithm in the book looks simple but in converting it to SL script there are some issues surrounding the direction element of the gun.

The Ray Sphere Test algorithm is:--

RAY is defined as:--

X = Rorg.x + Rdir.x * lamda
Y = Rorg.y + Rdir.y * lamda
Z = Rorg.z + Rdir.z * lamda


SPHERE is defined by:--

(X-CenterX)^2 + (Y-CenterY)^2 + (Z-CenterZ)^2 = Radius^2


the equations to be solved are

1) A = Rdir.x^2 + Rdir.y^2 + Rdir.z^2

2) B = 2 * (Rdir.x^2*(Rorg.x-CenterX) + (Rdir.y^2*(Rorg.y-CenterY) + (Rdir.z^2*
(Rorg.z-CenterZ))

3) C = (Rorg.x-CenterX)^2 + (Rorg.y-CenterY)^2 + (Rorg.z-CenterZ)^2 - Radius^2

often in part 1, A is set to 1 because the ray's direction is normalised.

The above is a quadratic equation with it being necessary to solve the determinant
B^2 - 4*A * C

if the determinant is -ve then the ray missed the sphere, otherwise it hit the sphere.

The above algorithm is implememented in the Popgun script below but it does not work as the gun direction variable is incorrectly set.


CODE

//
// Popgun
//
// This script is a basic gun- it waits for a mouseclick in mouselook and
// then fires a bullet in the direction the user is facing.
// It also animates the avatar holding it, to create a convining hold and
// firing look for the gun.
//
// This script can be used as a good basis for other weapons.
//

vector targetG = <108.0, 226.0, 128.0>; // Coords of sphere - radius = 1


float SPEED = 20.0; // Speed of arrow in meters/sec
integer LIFETIME = 7; // How many seconds will bullets live
// before deleting themselves
float DELAY = 0.2; // Delay between shots to impose

vector directG; // Direction gun points

vector velG; // Used to store velocity of arrow to be shot
vector posG; // Used to store position of arrow to be shot
rotation rotG; // Used to store rotation of arrow to be shot

integer have_permissions = FALSE; // Indicates whether wearer has yet given permission
// to take over their controls and animation.

integer armed = TRUE; // Used to impose a short delay between firings

string instruction_held_1 = "Use Mouselook (press 'M') to shoot me.";
string instruction_held_2 = "Choose 'Detach' from my menu to take me off.";
// Echoed to wearer when they are holding the bow
string instruction_not_held = "Right-click (apple-click) me, and choose More > Wear' from the menu to use me.";
// Echoed to toucher if not worn


fire()
{
//
// This subroutine creates and fires an arrow
//
if (armed)
{
//
// Actually fires the arrow
//
armed = FALSE;
rotG = llGetRot(); // Get current avatar mouselook direction
velG = llRot2Fwd(rotG); // Convert rotation to a direction vector

directG=velG;

posG = llGetPos(); // Get position of avatar to create arrow
posG = posG + velG; // Create arrow slightly in direction of travel
posG.z += 0.75; // Correct creation point upward to eye point
// from hips, so that in mouselook we see arrow
// travelling away from the camera.



velG = velG * SPEED; // Multiply normalized vector by speed

//llStartAnimation("shoot_R_handgun"); // Trigger the bow release animation
llTriggerSound("shoot", 1.0); // Make the sound of the arrow being shot
llRezObject("bullet", posG, velG, rotG, LIFETIME);
// Create the actual arrow from object
// inventory, and set its position, velocity,
// and rotation. Pass a parameter to it to
// tell it how long to live.

if (RaySphereTestFN()<0) llOwnerSay("Missed");
else llOwnerSay("Hit");

llSetTimerEvent(DELAY); // Wait until can fire again
}
}

integer RaySphereTestFN()
{ integer result;
float a;
float b;
float c;
float t1;
float t2;
float t3;
float radius;
float det;

// A
a=(directG.x*directG.x)+(directG.y*directG.y)+(directG.z*directG.z);

// B
t1=(directG.x*directG.x)*(posG.x-targetG.x);
t2=(directG.y*directG.y)*(posG.y-targetG.y);
t3=(directG.z*directG.z)*(posG.z-targetG.z);
b=2*(t1+t2+t3);

// C
t1=(posG.x-targetG.x)*(posG.x-targetG.x);
t2=(posG.y-targetG.y)*(posG.y-targetG.y);
t3=(posG.z-targetG.z)*(posG.z-targetG.z);
radius=1.0;
c=t1+t2+t3-radius;

// Det
det=(b*b)-(4*a*c);
if (det<0) result=-1;
else result=1;

return result;
}

default
{
state_entry()
//
// This routine is called whenever the script is edited and restarted. So if you
// are editing the bow while wearing it, this code will re-request permissions
// to animate and capture controls.
//
{
if (!have_permissions)
{
llRequestPermissions(llGetOwner(),
PERMISSION_TRIGGER_ANIMATION| PERMISSION_TAKE_CONTROLS);
}
}
on_rez(integer param)
{
//
// Called when the gun is created from inventory.
//
llPreloadSound("shoot"); // Preload shooting sound so you hear it
}

run_time_permissions(integer permissions)
{
//
// This routine is called when the user accepts the permissions request
// (sometimes this is automatic)
// so on receiving permissions, start animation and take controls.
//
if (permissions == PERMISSION_TRIGGER_ANIMATION| PERMISSION_TAKE_CONTROLS)
{
if (!have_permissions)
{
llWhisper(0, instruction_held_1);
llWhisper(0, instruction_held_2);
}
llTakeControls(CONTROL_ML_LBUTTON, TRUE, FALSE);
llStartAnimation("hold_R_handgun");
have_permissions = TRUE;
}
}

attach(key attachedAgent)
{
//
// If attached/detached from agent, change behavior
//
if (attachedAgent != NULL_KEY)
{
// Bow has been attached or rezzed from inventory, so
// ask for needed permissions.
llRequestPermissions(llGetOwner(),
PERMISSION_TRIGGER_ANIMATION| PERMISSION_TAKE_CONTROLS);
}
else
{
// Bow has been detached from avatar, so stop animation and release controls
if (have_permissions)
{
llStopAnimation("hold_R_handgun");
llStopAnimation("aim_R_handgun");
llReleaseControls();
llSetRot(<0,0,0,1>);
have_permissions = FALSE;
}
}
}

control(key name, integer levels, integer edges)
{
// This function is called when the mouse button or other controls
// are pressed, and the controls are being captured.
//
// Note the logical AND (single &) used - the levels and edges
// variables passed in are bitmasks, and must be checked with
// logical compare.
//
// Checking for both edge and level means that the button has just
// been pushed down, which is when we want to fire the arrow!
//
if ( ((edges & CONTROL_ML_LBUTTON) == CONTROL_ML_LBUTTON)
&&((levels & CONTROL_ML_LBUTTON) == CONTROL_ML_LBUTTON) )
{
// If left mousebutton is pressed, fire arrow
fire();
}
}

touch_start(integer num)
{
// If touched, remind user how to enter mouselook and shoot
if (have_permissions)
{
llWhisper(0, instruction_held_1);
llWhisper(0, instruction_held_2);
}
else
{
llWhisper(0, instruction_not_held);
}
}

timer()
{
// After timer expires, allow user to shoot bow again
llSetTimerEvent(0.0);
armed = TRUE;
}

}
Thanto Usitnov
Lord Byron wannabe
Join date: 4 Aug 2006
Posts: 68
01-04-2009 07:19
Hmm... I don't see anything missing. I'd suggest using some debug outputs. When you fire, is the avatar's rotation being grabbed correctly? Is the rotation being properly converted into a pointing vector? Is the RaySphereTestFN properly getting the rotation pointing vector AND the position vector of the target? The way I usually do it is that I put debug outputs after every bit of significant math, and both outside right before and inside right after for if statements. So, basically, for if statements, I output what's being checked, then the value of the comparison, then I output whether or not the if statement passed or failed.

So, yeah, debug statements should be very helpful.




Also, there may be a different test you can do (I'm not quite sure if this will work). First, get the rotation and position of the firing player, and then grab the position of the target. Create a vector from the difference between the player's position and the target's position. This vector points from the firing player to the target's center. Turn this into a rotation using llAxes2Rot (note: you'll need to generate an orthogonal vector for this to work - use llRot2Fwd(llAxisAngles2Rot(vector,0))). You can then compare the player's rotation to the one generated above with llAngleBetween. That should provide you with an angle between the two pointing vectors. I'm not quite sure what it outputs, though, so I may be wrong. Anyway, compare that to some set limit, and you should have your answer. Basically, this test determines (should, anyway) if a point is within a cone extending infinitely outward.
Macie Forzane
Registered User
Join date: 4 Jan 2009
Posts: 3
01-04-2009 10:44
From: Thanto Usitnov
Hmm... I don't see anything missing. I'd suggest using some debug outputs. When you fire, is the avatar's rotation being grabbed correctly? Is the rotation being properly converted into a pointing vector? Is the RaySphereTestFN properly getting the rotation pointing vector AND the position vector of the target? The way I usually do it is that I put debug outputs after every bit of significant math, and both outside right before and inside right after for if statements. So, basically, for if statements, I output what's being checked, then the value of the comparison, then I output whether or not the if statement passed or failed.

So, yeah, debug statements should be very helpful.




Also, there may be a different test you can do (I'm not quite sure if this will work). First, get the rotation and position of the firing player, and then grab the position of the target. Create a vector from the difference between the player's position and the target's position. This vector points from the firing player to the target's center. Turn this into a rotation using llAxes2Rot (note: you'll need to generate an orthogonal vector for this to work - use llRot2Fwd(llAxisAngles2Rot(vector,0))). You can then compare the player's rotation to the one generated above with llAngleBetween. That should provide you with an angle between the two pointing vectors. I'm not quite sure what it outputs, though, so I may be wrong. Anyway, compare that to some set limit, and you should have your answer. Basically, this test determines (should, anyway) if a point is within a cone extending infinitely outward.


Thank you for your helpful suggestions - its been good to get a fresh opinion.

I'll try these debug messages and vectors checks - then report back.
Hewee Zetkin
Registered User
Join date: 20 Jul 2006
Posts: 2,702
01-04-2009 12:00
Remember also that if you are ray casting, you are probably only interested in the FORWARD direction. That is, you don't necessarily care if the defined LINE intersects with the object, but if the RAY does. So if your discriminant is non-negative, you'll want to go ahead and solve for lambda to make sure there is a non-negative distance (ignore intersections behind the avatar).

Note also that projectiles GENERALLY don't follow a straight line unless they have buoyancy or a force set to counteract gravity.
Northwest Portland
Registered User
Join date: 31 Dec 2008
Posts: 2
01-06-2009 10:36
From: someone

vector target = <x, y, z> // target location
vector target_velocity = <x, y, z> // target velocity (duh)
float target_radius = 1.0; // radius of target sphere.
vector me = <x, y, z> // my location
vector projectile_velocity = <x, y, z> // the velocity a projectile would be rezzed with.

// This makes the calculation much simpler by changing the frame of reference so that the target appears stationary in relation to the projectile. Now it's a matter of calculating closest approach between a point and a ray, instead of two rays.
vector target_relative_velocity = projectile_velocity - target_velocity;

// Initial distance between you and the target. This becomes the hypotenuse of a right triangle.
float initial_distance = llVecDist(me, target);

// Use the dot product of the normalized vector to target and normalized projectile velocity to determine the angle of approach. I'm doing this from memory so it could be wrong but I think given two normalized vectors the dot product equals cos theta, and I want theta, so I take the arccos of the dot product.
float angle_of_approach = llAcos(llVecNorm(target - me) * llVecNorm(target_relative_velocity));

float distance_of_closest_approach;

if (angle_of_approach < PI_BY_TWO) // Make sure the projectile is headed toward the target.
{
// We have the hypotenuse and theta, so now it's just trig to determine distance of closest approach.
distance_of_closest_approach = llSin(angle_of_approach) * initial_distance;
}
else
{
// The projectile is already tangent or headed away from the target. We're not getting any closer than we already are.
distance_of_closest_approach = initial_distance;
}

if (distance_of_closest_approach < target_radius)
{
// Hit!
}


This is untested as I'm not in world right now, but I tried to break it down into logical steps. It "should" work.
Northwest Portland
Registered User
Join date: 31 Dec 2008
Posts: 2
01-06-2009 11:22
Here's a function version.

From: someone

float ClosestApproachDistance(vector target, vector target_velocity, vector me, vector projectile_velocity)
{
// This makes the calculation much simpler by changing the frame of reference so that the target appears stationary in relation to the projectile. Now it's a matter of calculating closest approach between a point and a ray, instead of two rays.
vector target_relative_velocity = projectile_velocity - target_velocity;

// Initial distance between you and the target. This becomes the hypotenuse of a right triangle.
float initial_distance = llVecDist(me, target);

// Use the dot product of the normalized vector to target and normalized projectile velocity to determine the angle of approach. I'm doing this from memory so it could be wrong but I think given two normalized vectors the dot product equals cos theta, and I want theta, so I take the arccos of the dot product.
float angle_of_approach = llAcos(llVecNorm(target - me) * llVecNorm(target_relative_velocity));

if (angle_of_approach < PI_BY_TWO) // Make sure the projectile is headed toward the target.
{
// We have the hypotenuse and theta, so now it's just trig to determine distance of closest approach.
return llSin(angle_of_approach) * initial_distance;
}
else
{
// The projectile is already tangent or headed away from the target. We're not getting any closer than we already are.
return initial_distance;
}
}


And then to use it.

From: someone

float target_radius = 1.0;

if (ClosestApproachDistance(<target>, <target_velocity>, <me>, <projectile_velocity>;) < target_radius)
{
// Hit!
}