• 🏆 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!

[vJASS] 3D Orbit Math

Status
Not open for further replies.
I hope this is okay to post in the trigger forums since it math + code.

I'm trying to figure out a way to make a 3D orbit system but since I'm not particularly good at math, I'm wondering if anyone here can lend me a hand. In my research I found out this equation on stackoverflow:
JASS:
x = x0 + r sin(θ) cos(φ)
y = y0 + r sin(θ) sin(φ)
z = z0 + r cos(θ)
So I tried to use that in this test:
JASS:
library SphereOrbit initializer init
   
    globals
        private constant string ORB_MODEL = "Abilities\\Weapons\\IllidanMissile\\IllidanMissile.mdl"
        private real a = 0.
        private real tilt = 60. * bj_DEGTORAD
        private unit array u
    endglobals
   
    private function Clock takes nothing returns nothing
       
        local real x
        local real y
        local real z
       
        local real r = 200.
       
        set a = a + 5. * bj_DEGTORAD
        //set tilt = 5. * bj_DEGTORAD //phi
       
        set x = 0. + r * Sin(a) * Cos(tilt)
        set y = 0. + r * Sin(a) * Sin(tilt)
        set z = 350. + r * Cos(a)
       
        call SetUnitX(u[0], x)
        call SetUnitY(u[0], y)
        call SetUnitFlyHeight(u[0], z, 0.)
       
    endfunction
   
    private function init takes nothing returns nothing
       
        local timer t = CreateTimer()
       
        set u[0] = CreateUnit(Player(0), 'dumi', 0., 0., 270.)
        call AddSpecialEffectTarget(ORB_MODEL, u[0], "origin")
       
        call TimerStart(t, .03125, true, function Clock)
        set t = null
       
    endfunction
   
endlibrary
Honestly I have no idea how the above formula is working. While this is in fact making a unit orbit another, it seems to only be able to do that in a vertical loop that can 'face' the angle the 'tilt' is set to. I think my approach is not good.

Ideally, the 3D orbit would have an orbit speed, an orbit distance, a 'facing' angle and a vertical tilt. This is how the customisation plays out in my head. I'm looking for a formula that will allow me to use those four values to obtain a 3D orbit. Basically, create a normal 2D orbit and tilt the Z axis. I'm not sure if approaching it that way is even feasible, so alternate solutions are welcome.
 
I think you already got it, or? ;p Here it's just a bit re-written:

change angle_speed_1 to make faster rotation for example
change angle_speed_2 to 0, to make for example "static orbiting", like 2D

JASS:
library SphereOrbit initializer init
  
    globals
        private constant string ORB_MODEL = "Abilities\\Weapons\\IllidanMissile\\IllidanMissile.mdl"
        private real angle_1       = 0.* bj_DEGTORAD // current rad position of the circle, e.g. at very top or very bottom
        private real angle_2       = 0.* bj_DEGTORAD // angle from orbiting towards the center, e.g. circleing east->west, or south->north
      
        private real angle_speed_1 = 4.* bj_DEGTORAD
        private real angle_speed_2 = 2.* bj_DEGTORAD
      
        private real distance_current = 200 // play around a bit with changing distance
        private real distance_min     = 50
        private real distance_max     = 400
        private real distance_delta   = 1
      
        private real height_offset    = 350
      
        private real center_x         = 0.
        private real center_y         = 0.
      
        private unit array u
    endglobals
  
    private function Clock takes nothing returns nothing
      
        local real x
        local real y
        local real z
      
        set distance_current = distance_current + distance_delta
        if distance_current > distance_max or  distance_current < distance_min then
            set distance_delta = -distance_delta
        endif
      
        set angle_1 = angle_1 + angle_speed_1
        set angle_2 = angle_2 + angle_speed_2
      
        set x = center_x + distance_current * Sin(angle_1) * Cos(angle_2)
        set y = center_y + distance_current * Sin(angle_1) * Sin(angle_2)
        set z = height_offset + distance_current * Cos(angle_1)
      
        call SetUnitX(u[0], x)
        call SetUnitY(u[0], y)
        call SetUnitFlyHeight(u[0], z, 0.)
      
    endfunction
  
    private function init takes nothing returns nothing

        local timer t = CreateTimer()
      
        set u[0] = CreateUnit(Player(0), 'dumi', 0., 0., 270.)
        call AddSpecialEffectTarget(ORB_MODEL, u[0], "origin")
      
        call TimerStart(t, .03125, true, function Clock)
        set t = null
      
    endfunction
  
endlibrary
 
Well, it's like a boat tipping over. Imagine the orbit is in 2D like it normally is, and then you tilt is sideways by 30°... which I just realised I typed 30% instead of 30°. Quelle est bête.

The way I see customisation for this would be to have an orbit with both pitch and yaw (or was that roll? not sure). Object in the orbit would have their orbit speed (aka changing their angle) and their distance from the center.
 
Last edited:
Level 37
Joined
Jul 22, 2015
Messages
3,485
Lol yeah 30° makes a lot more sense than 30%. If you are unfamiliar with the spherical coordinate system, just remember that θ is the angle you are familiar with (from positive x), and φ represents the angle from positive z. So if φ = 0° represents the positive z-axis, then φ = 90° represents positive y-axis, φ = 180° represents negative z-axis, and so on.

Anyway, the conversions you were given from cartesian to spherical coordinates don't look right. They should be like this:
JASS:
x = rcos(θ) sin(φ)
y = rsin(θ) sin(φ)
z = rcos(θ)
Normally z = rcos(φ), but since you want it to orbit like an atom, θ would be the way to go for that one.
 
Last edited:
The formula looks the same, but I'm not sure I understand. If feel like there should be 3 angles instead of 2? θ seems the angle of the orbiting object from the center, and φ is, if I'm not mistaken, a vertical tilt? What I'm missing from this is a horizontal tilt so that you can basically orient the obit in whatever angle/direction you want to.
 
The way I think of it, it seemed logical to add this 3rd angle that Spellbound was asking for, seemingly making the inputs more complicated -- but arriving at what he seemed to be describing as his goal.

This brought me to a code snippet I created which seems to get the desired results:
JASS:
library ThreeDimSpin
    globals
        constant real HALF_PI = bj_PI/2
        constant real PI = bj_PI
    endglobals

    struct Vector
        real x
        real y
        real z
        public static method create takes real facing, real tilt returns Vector
            local Vector newVec = .allocate()
            local real sinTilt = Sin(tilt)
            set newVec.x = Cos(facing) * sinTilt
            set newVec.y = Sin(facing) * sinTilt
            set newVec.z = Cos(tilt)
            return newVec
        endmethod
        public static method createXYZ takes real x, real y, real z returns Vector
            local Vector newVec = .allocate()
            set newVec.x = x
            set newVec.y = y
            set newVec.z = z
            return newVec
        endmethod
    endstruct

    function ComputePosition takes real facing, real tilt, real spin, real radius, real height returns Vector
        local real sinFacing = Sin(facing)
        local real cosFacing = Cos(facing)
        local real sinTilt = Sin(tilt)
        local real cosTilt = Cos(tilt)
        local real bComponent // used later
        local real cComponent // used later
        // let A be the vector of our axis of rotation
        local Vector a = Vector.create(facing, tilt)
        local Vector b
        local Vector c
        // let B be the perpendicular to our axis, with:
        //   - a tilt of "HALF_PI - tilt" which you can think of a 90 deg minus tilt
        //   - and a facing of "facing + PI" which is just 180 degrees in the other direction
        local real facingB = facing + PI
        local real tiltB = HALF_PI - tilt
        // maybe this will result in a negative tiltB
        if tiltB < 0 then
            set tiltB = tiltB + PI
            set facingB = facing
        endif
        // now we have the direction data for a perpendicular vector (no distance yet)
        set b = Vector.create(facingB, tiltB)
        // then we take their cross product
        set c = Vector.createXYZ(a.y * b.z - b.y * a.z, b.x * a.z - a.x * b.z, a.x * b.y - b.x * a.y)
        // then we will add b * Sin(spin) * radius + c * Cos(spin) * radius
        // make sure to compute everything only once, so I create a local for bComponent and cComponent
        set bComponent = Sin(spin) * radius
        set cComponent = Cos(spin) * radius
        set a.x = b.x * bComponent + c.x * cComponent
        set a.y = b.y * bComponent + c.y * cComponent
        set a.z = b.z * bComponent + c.z * cComponent + height
        call b.destroy()
        call c.destroy()
        return a
    endfunction
endlibrary

(feel free to change any function names/design in this snippet, it was just a draft of an implementation of the logic)

I also created an image to try to give a visual understanding of the math that I was trying to do. See below (read left to right):

3dspin-png.294389
 

Attachments

  • 3dSpin.png
    3dSpin.png
    761.5 KB · Views: 399

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
Construct a 3x3 rotation matrix and multiply it by the displacement vector. Since this vector can be defined as D in one axis and 0 in the others, the end result is that one can heavily optimize away the rotation matrix by removing the Y and Z columns. Do this and you get a formula like the one you use with 2 input rotation angles.

What happened to the third rotation angle? I believe this is the result of Gimbal Lock. If one wants that extra degree of freedom maintained one either has to use quaternions instead, which do not suffer gimbal lock, or define the displacement vector in the Y or Z axis and not the X axis.

If one defines the displacement in the Y axis the formula becomes...
X = D * (cos(a) * sin(b) * sin(c) - sin(a) * cos(c))
Y = D * (sin(a) * sin(b) * sin(c) + cos(a) * cos(c))
Z = D * cos(b) * sin(c)
With a, b and c being angles. All 3 degrees of freedom are available.

If this can represent unique angles that the current approach cannot is questionable.

Oh and the correct formula if displacement in the X axis...
X = D * cos(a) * cos(b)
Y = D * sin(a) * cos(b)
Z = D * -sin(b)
This looks similar to what is posted above except the rotation might be defined differently.
 
Recenly talked with SpellBound on discord, so here's also an other approach:

JASS:
library Orbit uses TimerUtils
    struct Orbit
        private static constant real INTERVAL        = 0.031250000            
        private static constant real SQUARE_ROOT_TWO = SquareRoot(2)
      
        real phi                    // circle position
        real phi_speed              // circulation speed
      
        // alpha = X-rotation
        real alpha_frequence_factor // cosinus frequence which describes the speed of alpha change
        real alpha_amplitude        // maximum range of alpha-value
      
        // beta = Y-rotation
        real beta_frequence_factor  // cosinus frequence which describes the speed of beta change
        real beta_amplitude         // maximum range of beta-value
      
        // gamma = Z-rotation
        real gamma_frequence_factor // cosinus frequence which describes the speed of gamma change
        real gamma_amplitude        // maximum range of gamma-value
      
        // we can use either plain coordinates or a unit, or an effect as center
        // priorities, which gets favaored by code:
        // 1. unit
        // 2. effect
        // 3. xyz
        real x_center
        real y_center
        real z_center
        unit unit_center
        effect effect_center

        real radius
        unit object_unit        // rotating unit
        effect object_effect    // rotating effect
      
        private timer clock
        static method update takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            local real x_new
            local real y_new
            local real z_new
            local real x_new_2
            local real y_new_2
            local real alpha
            local real beta
            local real gamma
          
            set .phi = .phi + .phi_speed
            if .unit_center != null then
                set .x_center = GetUnitX(.unit_center)
                set .y_center = GetUnitY(.unit_center)
                set .z_center = GetUnitFlyHeight(.unit_center)
            elseif effect_center != null then
                set .x_center = BlzGetLocalSpecialEffectX(effect_center)
                set .y_center = BlzGetLocalSpecialEffectY(effect_center)
                set .z_center = BlzGetLocalSpecialEffectZ(effect_center)
            endif
          
            // -> we use Cos to set alpha/beta/gamma constant 1 if factor_value is 0 (not maintained)
            set alpha = .alpha_amplitude*Cos(.phi*.alpha_frequence_factor)
            set beta  = .beta_amplitude*Cos(.phi*.beta_frequence_factor)
            set gamma = .gamma_amplitude*Cos(.phi*.gamma_frequence_factor)
          
            set x_new = .radius * ((Cos(.phi) * Cos(beta)) + Sin(phi)*Sin(alpha)*Sin(beta)) / SQUARE_ROOT_TWO
            set y_new = .radius * Sin(.phi) * Cos(alpha) / SQUARE_ROOT_TWO
            set z_new = .z_center + .radius * ((-Cos(.phi) * Sin(beta)) + Sin(.phi)*Sin(alpha)*Cos(beta)) / SQUARE_ROOT_TWO
          
            // for Z rotation only
            set x_new_2 = .x_center + x_new * Cos(gamma) - y_new * Sin(gamma)
            set y_new_2 = .y_center + x_new * Sin(gamma) + y_new * Cos(gamma)
          
            if .object_unit != null then
                call SetUnitX(.object_unit, x_new_2)
                call SetUnitY(.object_unit, y_new_2)
                call SetUnitFlyHeight(.object_unit, z_new, 0)
            endif
            if .object_effect != null then
                call BlzSetSpecialEffectPosition(.object_effect, x_new_2, y_new_2, z_new)
                // +bj_PI/2 -> means face into current direction
                // +bj_PI   -> face into center
                // etc
                call BlzSetSpecialEffectOrientation(.object_effect, alpha, beta, (phi+bj_PI/2)+gamma)
            endif
        endmethod
      
        method destroy takes nothing returns nothing
            set .object_unit   = null
            set .unit_center   = null
            set .effect_center = null
            call ReleaseTimer(.clock)
            call deallocate()
        endmethod
      
        static method create takes effect e, unit u returns thistype
            local thistype this = allocate()
            set .clock  = NewTimerEx(this)
            set .object_effect = e
            set .object_unit = u
            call TimerStart(.clock, INTERVAL, true, function thistype.update)
            return this
        endmethod
    endstruct
endlibrary
JASS:
scope OrbitDemo initializer Init

globals
    private constant string EFFECT_PATH = "units\\human\\Knight\\Knight.mdl"
endglobals

private function Init takes nothing returns nothing
        local Orbit this
        local effect center
        local effect fx
      
        // particle 1 rotates around point 0/0/300
      
        set fx = AddSpecialEffect(EFFECT_PATH, 0, 0)
        set this                        = Orbit.create(fx, null)
        set this.phi                    = bj_DEGTORAD* 30
        set this.phi_speed              = bj_DEGTORAD* 1
        set this.alpha_amplitude        = bj_DEGTORAD* 50
        set this.alpha_frequence_factor = 1.2
        set this.beta_amplitude         = bj_DEGTORAD* 0
        set this.beta_frequence_factor  = 0
        set this.gamma_amplitude        = bj_DEGTORAD* 0
        set this.gamma_frequence_factor = 0
        set this.x_center = 0
        set this.y_center = 0
        set this.z_center = 300
        set this.radius   = 500
      
        set center = this.object_effect
      
        // particle 2 rotates around particle 1
        set fx = AddSpecialEffect(EFFECT_PATH, 0, 0)
        set this                        = Orbit.create(fx, null)
        set this.phi                    = bj_DEGTORAD* 30
        set this.phi_speed              = bj_DEGTORAD* 2
        set this.alpha_amplitude        = bj_DEGTORAD* 20
        set this.alpha_frequence_factor = 0
        set this.beta_amplitude         = bj_DEGTORAD* 10
        set this.beta_frequence_factor  = 0
        set this.gamma_amplitude        = bj_DEGTORAD* 0
        set this.gamma_frequence_factor = 0
        set this.radius   = 250
        set this.effect_center = center
      
        // particle 3 rotates around particle 1
        set fx = AddSpecialEffect(EFFECT_PATH, 0, 0)
        set this                        = Orbit.create(fx, null)
        set this.phi                    = bj_DEGTORAD* 30
        set this.phi_speed              = bj_DEGTORAD* 3
        set this.alpha_amplitude        = bj_DEGTORAD* 0
        set this.alpha_frequence_factor = 0
        set this.beta_amplitude         = bj_DEGTORAD* 50
        set this.beta_frequence_factor  = 0
        set this.gamma_amplitude        = bj_DEGTORAD* 0
        set this.gamma_frequence_factor = 0
        set this.radius   = 400
        set this.effect_center = center
      
        // particle 4 rotates around particle 1
        set fx = AddSpecialEffect(EFFECT_PATH, 0, 0)
        set this                        = Orbit.create(fx, null)
        set this.phi                    = bj_DEGTORAD* 30
        set this.phi_speed              = bj_DEGTORAD* 2
        set this.alpha_amplitude        = bj_DEGTORAD* 70
        set this.alpha_frequence_factor = 0
        set this.beta_amplitude         = bj_DEGTORAD* 90
        set this.beta_frequence_factor  = 0
        set this.gamma_amplitude        = bj_DEGTORAD* 0
        set this.gamma_frequence_factor = 0
        set this.radius   = 300
        set this.effect_center = center
endfunction
endscope
 

Attachments

  • Orbit.w3x
    40.5 KB · Views: 41
Seemingly the gamma orientation is not right, this last computation here:

call BlzSetSpecialEffectOrientation(.object_effect, alpha, beta, (phi+bj_PI/2)+gamma)

^as I ignored phi and gamma come from different coordination system (phi=local, gamma=global). Rest works fine, but I'll simply try to get my head around a fix next days and then post it as system, when fixed and with some more description.
 
Status
Not open for further replies.
Top