• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[JASS] Deflection Off Cliffs

Status
Not open for further replies.
Level 12
Joined
Mar 28, 2005
Messages
160
in reference to my resource here

we can deflect off other units/buildings/destructables without much hassle

doing so using said algorithm when approaching a cliff, not so much

the problem lies in the fact that we can strike various angled cliffs when traveling in the same direction - so we need to know exactly what angle the cliff is at, or whereabouts on that cliff we struck, to accurately predict deflection angles.

the approach I recently took was to enumerate in a circle of small radius (~32.) from negative to positive bj_PI/2 radians about the missile with respect for its facing (or velocity vector angle), in 1/16th Pi intervals (bj_PI/16), assuming that it won't strike something in a half circle behind its direction of travel (really only to reduce overhead)

the idea was to grab the specific x/y coord from that array which had the highest locationz value, and select it as our spot for deflection, and push it through our deflection angle algorithm. hoping that where we strike the cliff is indeed the highest point in the list, or at least close to it.

starting out initially as a hunch, it actually seems to work with seemingly 100% accuracy (much to my surprise). several dozen collisions on a few different cliffs have resulted in very realistic deflection angles

thoughts on this method, or other such valid methods.

thanks

P.S. in case you are interested in how I determine what is or isn't a cliff

I am looking for when my missile reaches a point where its zloc-collision_radius is <= the zloc at a point in a half circle in its facing direction with radius collision_radius

This would be when the missile strikes a surface, whether it be the ground, or a cliff. To differentiate between the two, I assume that the zloc value as you approach a cliff should increase in height much faster than would the zloc value as you approach the ground - determining a slope somewhere in the middle of these extremes, a check for changes above or below said value allows for such discrimination. Random jaggies along the ground could botch this method, so I keep a running total of the last few zloc heights and establish a slope across a slightly larger distance to help alleviate random blips from the terrain.

-Hulk3
 
Last edited:
Level 12
Joined
Mar 28, 2005
Messages
160
here is an example of this in action

JASS:
local real array r
                
set r[1]=GetLocationZ(UnitLoc)+GetUnitFlyHeight(Unit)-UnitRadius // height at or above which the missile will collide with given terrain loc
set r[2]=.ang-HALFPI
set r[3]=.ang+HALFPI
// loop in a half circle using the missiles facing
loop
    exitwhen r[2]>r[3] // stop after half circle
    set r[4]=UnitX+UnitRadius*Cos(r[2])
    set r[5]=UnitY+UnitRadius*Sin(r[2])
    call MoveLocation(LOCATION,r[4],r[5])
    set r[6]=GetLocationZ(LOCATION)
    if r[6]>=r[1] and r[6]>r[9] then // collision found, and better than the last
        // update dynamic array members
        set r[7]=r[4] // x
        set r[8]=r[5] // y
        // x/y are not used here, but they would be if you wanted to say, deflect from those coords.
        set r[9]=r[6] // z
    endif
    set r[2]=r[2]+FRACPI // continue to loop, FRACPI is ideally something like bj_PI/16.
    // the smaller it is, theoretically, the more accurately the collision point is identified
endloop
if r[9]>0. then // we have a collision
    call MoveLocation(LOCATION,UnitX,UnitY)
    if r[9]-GetLocationZ(LOCATION)>=10. then 
    // cliff collision
    // after some experimentation
    // collisions involving cliffs generally produce height increases>10.
    // usually >15.
    // whereas ground collisions are generally much lower than 10.
    // usually <6.
    else 
    // ground collision
    endif
endif
this method is surprisingly accurate, IMO

could be a supplement to my DeflectionAngle script
 
Level 18
Joined
Jan 21, 2006
Messages
2,552
So basically this problem is entirely because of your lack of calculation of the normal vector from the terran, which gives you its entire orientation. I suggest you begin to use vectors. The above code looks way more complicated than the vector approach. There are also already vector libraries out there that allow you to get the terrain normal, such as Anitarf's.

JASS:
library Vector

//*****************************************************************
//*  VECTOR LIBRARY
//*
//*  written by: Anitarf
//*
//*  The library contains a struct named vector, which represents a
//*  point in 3D space. As such, it has three real members, one for
//*  each coordinate: x, y, z. It also has the following methods:
//*
//*        static method create takes real x, real y, real z returns vector
//*  Creates a new vector with the given coordinates.
//*
//*        method getLength takes nothing returns real
//*  Returns the length of the vector it is called on.
//*
//*        static method sum takes vector augend, vector addend returns vector
//*  Returns the sum of two vectors as a new vector.
//*
//*        method add takes vector addend returns nothing
//*  Similar to sum, except that it doesn't create a new vector for the result,
//*  but changes the vector it is called on by adding the "added" to it.
//*
//*        static method difference takes vector minuend, vector subtrahend returns vector
//*  Returns the difference between two vectors as a new vector.
//*
//*        method subtract takes vector subtrahend returns nothing
//*  Similar to difference, except that it doesn't create a new vector for the result,
//*  but changes the vector it is called on by subtracting the "subtrahend" from it.
//*
//*        method scale takes real factor returns nothing
//*  Scales the vector it is called on by the given factor.
//*
//*        method setLength takes real length returns nothing
//*  Sets the length of the vector it is called on to the given value, maintaining its orientation.
//*
//*        static method dotProduct takes vector a, vector b returns real
//*  Calculates the dot product (also called scalar product) of two vectors.
//*
//*        static method crossProduct takes vector a, vector b returns vector
//*  Calculates the cross product (also called vector product) of two vectors
//*  and returns it as a new vector.
//*
//*        static method tripleProductScalar takes vector a, vector b, vector c returns real
//*  Calculates the triple scalar product of three vectors.
//*
//*        static method tripleProductVector takes vector a, vector b, vector c returns vector
//*  Calculates the triple vector product of three vectors and returns it as a new vector.
//*
//*
//*        static method projectionVector takes vector projected, vector direction returns vector
//*  Calculates the projection of the vector "projected" onto the vector "direction"
//*  and returns it as a new vector.
//*  Returns null if the vector "direction" has a length of 0.
//*
//*        method projectVector takes vector direction returns nothing
//*  Projects the vector it is called on onto the vector "direction".
//*  Does nothing if the vector "direction" has a length of 0.
//*
//*        static method projectionPlane takes vector projected, vector normal returns vector
//*  Calculates the projection of the vector "projected" onto a plane defined by
//*  its normal vector and returns it as a new vector.
//*  Returns null if the vector "normal" has a length of 0.
//*
//*        method projectPlane takes vector normal returns nothing
//*  Projects the vector it is called on onto a plane defined by its normal vector.
//*  Does nothing if the vector "normal" has a length of 0.
//*
//*        static method getAngle takes vector a, vector b returns real
//*  Returns the angle between two vectors, in radians, returns a value between 0 and pi.
//*  Returns 0.0 if any of the vectors are 0 units long.
//*
//*        method rotate takes vector axis, real angle returns nothing
//*  Rotates the vector it is called on around the axis defined by the vector "axis"
//*  by the given angle, which should be input in radians.
//*  Does nothing if axis is 0 units long.
//*
//*
//*        static method createTerrainPoint takes real x, real y returns vector
//*  Creates a vector to the given terrain coordinate, taking its z height into account.
//*
//*        method getTerrainPoint takes real x, real y returns nothing
//*  Sets the vector it is called on to the given terrain coordinate, taking its z height into account.
//*
//*        static method createTerrainNormal takes real x, real y, real sampleRadius returns vector
//*  Creates the normal vector of the terrain at given coordinates. "sampleRadius" defines
//*  how far apart the reference points will be, if they are further apart, the result will
//*  be an impression of smoother terrain; normaly the value should be between 0 and 128.
//*
//*        method getTerrainNormal takes real x, real y, real sampleRadius returns nothing
//*  Sets the vector it is called on to the normal of the terrain at given coordinates.
//*
//*
//*        method isInCylinder takes vector cylinderOrigin, vector cylinderHeight, real cylinderRadius returns boolean
//*  Determines if a point is within a given cylinder. The cylinder's origin vector points
//*  to the center of one of the two paralel circular sides, and the height vector points
//*  from the origin point to the center of the other of the two paralel circular sides.
//*  Returns false if the point is not in the cylinder or if the vector cylinderHeight is 0 units long.
//*
//*        method isInCone takes vector coneOrigin, vector coneHeight, real coneRadius returns boolean
//*  Determines if a point is within a given cone. The cone's origin vector points to the
//*  center of the circular side, and the height vector points from the origin point to
//*  the tip of the cone.
//*  Returns false if the point is not in the cylinder or if the vector coneHeight is 0 units long.
//*
//*        method isInSphere takes vector sphereOrigin, real sphereRadius returns boolean
//*  Determines if a point is within a give sphere. The sphere's origin vector points to the
//*  center of the sphere.
//*  Returns false if the point is not in the sphere.
//*****************************************************************

    struct vector
        real x
        real y
        real z
        
        static method create takes real x, real y, real z returns vector
            local vector v = vector.allocate()
            set v.x=x
            set v.y=y
            set v.z=z
            return v
        endmethod
        
        method getLength takes nothing returns real
          return SquareRoot(.x*.x + .y*.y + .z*.z)
        endmethod
        
        static method sum takes vector augend, vector addend returns vector
            local vector v = vector.allocate()
            set v.x = augend.x+addend.x
            set v.y = augend.y+addend.y
            set v.z = augend.z+addend.z
            return v
        endmethod
        method add takes vector addend returns nothing
            set this.x=this.x+addend.x
            set this.y=this.y+addend.y
            set this.z=this.z+addend.z
        endmethod
        
        static method difference takes vector minuend, vector subtrahend returns vector
            local vector v = vector.allocate()
            set v.x = minuend.x-subtrahend.x
            set v.y = minuend.y-subtrahend.y
            set v.z = minuend.z-subtrahend.z
            return v
        endmethod
        method subtract takes vector subtrahend returns nothing
            set this.x=this.x-subtrahend.x
            set this.y=this.y-subtrahend.y
            set this.z=this.z-subtrahend.z
        endmethod
        
        method scale takes real factor returns nothing
            set this.x=this.x*factor
            set this.y=this.y*factor
            set this.z=this.z*factor
        endmethod
        
        method setLength takes real length returns nothing
            local real l = SquareRoot(.x*.x + .y*.y + .z*.z)
            if l == 0.0 then
                debug call BJDebugMsg("vector.setLength error: The length of the vector is 0.0!")
                return
            endif
            set l = length/l
            set this.x = this.x*l
            set this.y = this.y*l
            set this.z = this.z*l
        endmethod
        
        static method dotProduct takes vector a, vector b returns real
            return (a.x*b.x+a.y*b.y+a.z*b.z)
        endmethod
        
        static method crossProduct takes vector a, vector b returns vector
            local vector v = vector.allocate()
            set v.x = a.y*b.z - a.z*b.y
            set v.y = a.z*b.x - a.x*b.z
            set v.z = a.x*b.y - a.y*b.x
            return v
        endmethod

        static method tripleProductScalar takes vector a, vector b, vector c returns real
            return ((a.y*b.z - a.z*b.y)*c.x+(a.z*b.x - a.x*b.z)*c.y+(a.x*b.y - a.y*b.x)*c.z)
        endmethod

        static method tripleProductVector takes vector a, vector b, vector c returns vector
            local vector v = vector.allocate()
            local real n = a.x*c.x+a.y*c.y+a.z*c.z
            local real m = a.x*b.x+a.y*b.y+a.z*b.z
            set v.x = b.x*n-c.x*m
            set v.y = b.y*n-c.y*m
            set v.z = b.z*n-c.z*m
            return v
        endmethod

// ================================================================

        static method projectionVector takes vector projected, vector direction returns vector
            local vector v = vector.allocate()
            local real l = direction.x*direction.x+direction.y*direction.y+direction.z*direction.z
            if l == 0.0 then
                call v.destroy()
                debug call BJDebugMsg("vector.projectionVector error: The length of the direction vector is 0.0!")
                return 0
            endif
            set l = (projected.x*direction.x+projected.y*direction.y+projected.z*direction.z) / l
            set v.x = direction.x*l
            set v.y = direction.y*l
            set v.z = direction.z*l
            return v
        endmethod
        method projectVector takes vector direction returns nothing
            local real l = direction.x*direction.x+direction.y*direction.y+direction.z*direction.z
            if l == 0.0 then
                debug call BJDebugMsg("vector.projectVector error: The length of the direction vector is 0.0!")
                return
            endif
            set l = (this.x*direction.x+this.y*direction.y+this.z*direction.z) / l
            set this.x = direction.x*l
            set this.y = direction.y*l
            set this.z = direction.z*l
        endmethod

        static method projectionPlane takes vector projected, vector normal returns vector
            local vector v = vector.allocate()
            local real l = normal.x*normal.x+normal.y*normal.y+normal.z*normal.z
            if l == 0.0 then
                call v.destroy()
                debug call BJDebugMsg("vector.projectionPlane error: The length of the normal vector is 0.0!")
                return 0
            endif
            set l = (projected.x*normal.x+projected.y*normal.y+projected.z*normal.z) / l
            set v.x = projected.x - normal.x*l
            set v.y = projected.y - normal.y*l
            set v.z = projected.z - normal.z*l
            return v
        endmethod
        method projectPlane takes vector normal returns nothing
            local real l = normal.x*normal.x+normal.y*normal.y+normal.z*normal.z
            if l == 0.0 then
                debug call BJDebugMsg("vector.projectPlane error: The length of the normal vector is 0.0!")
                return
            endif
            set l = (this.x*normal.x+this.y*normal.y+this.z*normal.z) / l
            set this.x = this.x - normal.x*l
            set this.y = this.y - normal.y*l
            set this.z = this.z - normal.z*l
        endmethod

        static method getAngle takes vector a, vector b returns real
            local real l = SquareRoot(a.x*a.x + a.y*a.y + a.z*a.z)*SquareRoot(b.x*b.x + b.y*b.y + b.z*b.z)
            if l == 0 then
                debug call BJDebugMsg("vector.getAngle error: The length of at least one of the vectors is 0.0!")
                return 0.0
            endif
            return Acos((a.x*b.x+a.y*b.y+a.z*b.z)/l) //angle is returned in radians
        endmethod
        
        method rotate takes vector axis, real angle returns nothing //angle is taken in radians
            local real xx
            local real xy
            local real xz
            local real yx
            local real yy
            local real yz
            local real zx
            local real zy
            local real zz
            local real al = axis.x*axis.x+axis.y*axis.y+axis.z*axis.z //axis length^2
            local real f
            local real c = Cos(angle)
            local real s = Sin(angle)
            if al == 0.0 then
                debug call BJDebugMsg("vector.rotate error: The length of the axis vector is 0.0!")
                return
            endif
            set f = (this.x*axis.x+this.y*axis.y+this.z*axis.z) / al
            set zx = axis.x*f
            set zy = axis.y*f
            set zz = axis.z*f //axis component of rotated vector
            set xx = this.x-zx
            set xy = this.y-zy
            set xz = this.z-zz //component of vector perpendicular to axis
            set al = SquareRoot(al)
            set yx = (axis.y*xz - axis.z*xy)/al
            set yy = (axis.z*xx - axis.x*xz)/al //y same length as x by using cross product and dividing with axis length
            set yz = (axis.x*xy - axis.y*xx)/al //x,y - coordinate system in which we rotate
            set this.x=xx*c+yx*s+zx
            set this.y=xy*c+yy*s+zy
            set this.z=xz*c+yz*s+zz
        endmethod
        
// ================================================================

        private static location loc = Location(0.0,0.0)

        static method createTerrainPoint takes real x, real y returns vector
            local vector v = vector.allocate()
            call MoveLocation(vector.loc,x,y)
            set v.x=x
            set v.y=y
            set v.z=GetLocationZ(loc)
            return v
        endmethod
        method getTerrainPoint takes real x, real y returns nothing
            call MoveLocation(vector.loc,x,y)
            set this.x=x
            set this.y=y
            set this.z=GetLocationZ(loc)
        endmethod

        static method createTerrainNormal takes real x, real y, real sampleRadius returns vector
            local vector v = vector.allocate()
            local real zx
            local real zy
            call MoveLocation(vector.loc, x-sampleRadius, y)
            set zx=GetLocationZ(vector.loc)
            call MoveLocation(vector.loc, x+sampleRadius, y)
            set zx=zx-GetLocationZ(vector.loc)
            call MoveLocation(vector.loc, x, y-sampleRadius)
            set zy=GetLocationZ(vector.loc)
            call MoveLocation(vector.loc, x, y+sampleRadius)
            set zy=zy-GetLocationZ(vector.loc)
            set sampleRadius=2*sampleRadius
            set v.x = zx*sampleRadius
            set v.y = zy*sampleRadius
            set v.z = sampleRadius*sampleRadius
            return v
        endmethod
        method getTerrainNormal takes real x, real y, real sampleRadius returns nothing
            local real zx
            local real zy
            call MoveLocation(vector.loc, x-sampleRadius, y)
            set zx=GetLocationZ(vector.loc)
            call MoveLocation(vector.loc, x+sampleRadius, y)
            set zx=zx-GetLocationZ(vector.loc)
            call MoveLocation(vector.loc, x, y-sampleRadius)
            set zy=GetLocationZ(vector.loc)
            call MoveLocation(vector.loc, x, y+sampleRadius)
            set zy=zy-GetLocationZ(vector.loc)
            set sampleRadius=2*sampleRadius
            set this.x = zx*sampleRadius
            set this.y = zy*sampleRadius
            set this.z = sampleRadius*sampleRadius
        endmethod

// ================================================================

        method isInCylinder takes vector cylinderOrigin, vector cylinderHeight, real cylinderRadius returns boolean
            local real l

            local real x = this.x-cylinderOrigin.x
            local real y = this.y-cylinderOrigin.y
            local real z = this.z-cylinderOrigin.z
            if x*cylinderHeight.x+y*cylinderHeight.y+z*cylinderHeight.z < 0.0 then //point below cylinder
                return false
            endif
            
            set x = x-cylinderHeight.x
            set y = y-cylinderHeight.y
            set z = z-cylinderHeight.z
            if x*cylinderHeight.x+y*cylinderHeight.y+z*cylinderHeight.z > 0.0 then //point above cylinder
                return false
            endif
            
            set l = cylinderHeight.x*cylinderHeight.x+cylinderHeight.y*cylinderHeight.y+cylinderHeight.z*cylinderHeight.z
            if l == 0.0 then
                debug call BJDebugMsg("vector.isInCylinder error: The length of the cylinderHeight vector is 0.0!")
                return false
            endif
            set l = (x*cylinderHeight.x+y*cylinderHeight.y+z*cylinderHeight.z) / l
            set x = x - cylinderHeight.x*l
            set y = y - cylinderHeight.y*l
            set z = z - cylinderHeight.z*l
            if x*x+y*y+z*z > cylinderRadius*cylinderRadius then //point outside cylinder
                return false
            endif
            
            return true
        endmethod

        method isInCone takes vector coneOrigin, vector coneHeight, real coneRadius returns boolean
            local real l

            local real x = this.x-coneOrigin.x
            local real y = this.y-coneOrigin.y
            local real z = this.z-coneOrigin.z
            if x*coneHeight.x+y*coneHeight.y+z*coneHeight.z < 0.0 then //point below cone
                return false
            endif
            
            set l = coneHeight.x*coneHeight.x+coneHeight.y*coneHeight.y+coneHeight.z*coneHeight.z
            if l == 0.0 then
                debug call BJDebugMsg("vector.isInCone error: The length of the coneHeight vector is 0.0!")
                return false
            endif
            set l = (x*coneHeight.x+y*coneHeight.y+z*coneHeight.z) / l
            set x = x - coneHeight.x*l
            set y = y - coneHeight.y*l
            set z = z - coneHeight.z*l
            if SquareRoot(x*x+y*y+z*z) > coneRadius*(1.0-l) then //point outside cone
                return false
            endif
            
            return true
        endmethod

        method isInSphere takes vector sphereOrigin, real sphereRadius returns boolean
            if sphereRadius*sphereRadius < ((this.x-sphereOrigin.x)*(this.x-sphereOrigin.x)+(this.y-sphereOrigin.y)*(this.y-sphereOrigin.y)+(this.z-sphereOrigin.z)*(this.z-sphereOrigin.z)) then
                return false
            endif
            return true
        endmethod
    endstruct

endlibrary
library VectorLib requires Vector // For backwards compatibility.
endlibrary

Once you get the terrain normal vector you can just rotate your motion vector around it by 180 (always) and then invert it so it faces the other direction. Simple as that. Inversion is just multiplying by a negative amount.
 
Level 12
Joined
Mar 28, 2005
Messages
160
example please of use, not sure I follow exactly what you are getting at

the problem is not solving for vector components - they are easy enough to back out of vel*ang anyways, and as you can see when I solved for DeflectionAngle, solving for the terrain normal would be no problem, if we knew where it was

the problem is deciding exactly where the missile has collided with the cliff - I thought I explained that well in the OP - how do you know what normal to solve for if you don't know where on the terrain you have struck?

unless I am mis-understanding what getTerrainNormal is doing in that script - I'd be all ears to hear what

I don't think this solution is all that complicated - take out the commments and its only 15 lines, which includes just some simple arithmetic
 
Level 18
Joined
Jan 21, 2006
Messages
2,552
the problem is deciding exactly where the missile has collided with the cliff - I thought I explained that well in the OP - how do you know what normal to solve for if you don't know where on the terrain you have struck?

Ah. I see. I'll get back to you.
 
Level 12
Joined
Mar 28, 2005
Messages
160
back yet?

slight update, and now actually a useable function

JASS:
globals
    // radius around units to check for collisions
    constant real COLLISION_RADIUS=64.
endglobals

function findCliffCollision takes unit u, real ang returns boolean
    // where unit u is the unit that is moving
    // and real ang is the angle in radians of its movement 
    // this could easily be adjusted for use with vector components of movement (x/y)
    local real array r
    
    set r[10]=GetUnitX(u)
    set r[11]=GetUnitY(u)
                
    call MoveLocation(TEMPLOC,r[10],r[11])
    // height at or above which the slider will collide with the given terrain TEMPLOC
    // RADIUS/2. because it just reacts better (long winded explanation upon request)
    set r[1]=GetLocationZ(TEMPLOC)+GetUnitFlyHeight(u)-(COLLISION_RADIUS/2.)
    set r[2]=.ang-.7854 // bj_PI/4.
    set r[3]=.ang+.7854
    // loop in a quarter circle
    loop
        exitwhen r[2]>r[3] // stop after the quarter circle
        set r[4]=r[10]+COLLISION_RADIUS*Cos(r[2])
        set r[5]=r[11]+COLLISION_RADIUS*Sin(r[2])
        call MoveLocation(TEMPLOC,r[4],r[5])
        set r[6]=GetLocationZ(TEMPLOC)
        // collision found, and better than the last
        if r[6]>=r[1] and r[6]>r[9] then
            // update array members dynamically
            set r[7]=r[4] // x
            set r[8]=r[5] // y
            // x/y are not used here, but they would be if you wanted to say, deflect from those coords.
            set r[9]=r[6] // z
        endif
        // continue to loop, bj_PI/16.
        set r[2]=r[2]+.1963 
        // the smaller our interval is, theoretically, the more accurately the collision point is identified
        // bj_PI/16. works just fine, and produces merely 8 points of reference as currently configured
    endloop
    call MoveLocation(TEMPLOC,r[10],r[11])
    // cliff???
    if r[9]>0. and r[9]-GetLocationZ(TEMPLOC)>=15. then    
        // a cliff collision has been detected
        // after some experimentation, I found that collisions involving cliffs generally produce height increases>15.
        // whereas ground collisions are generally much lower than 15., usually <6.
        return true
    endif    
    return false
endmethod

the more I think about it, the more I suspect this should be a wall collision, rather than a cliff collision
 
Level 12
Joined
Mar 28, 2005
Messages
160
to reiterate

I am searching for a point/s in a quarter circle surrounding the units facing (or traveling angle) at which it would collide, and tabulating the highest point (or suspected collision x/y) of these locs, then verifying it is indeed a cliff by calculating the slope of the line connecting the unit and the collision point.
 
Status
Not open for further replies.
Top