26
\$\begingroup\$

I have a 3D model of a turret that con rotate around the Y-axis. This turret has a cannon that is significantly off the center of the object. I want the cannon, not the turret, to aim at a specified target. I can only rotate the turret, however, and thus I don't know what equation I need to apply in order to accomplish by objective.

The following image illustrates my problem:enter image description here

If I have the turret "LookAt()" the target, a laser originating from the cannon will completely miss said target.

If this were a completely top-down scenario, and the cannon were exactly parallel to the turret, then my logic tells me that the fake target should be located at a position that's equal to the actual target plus an offset that's equal to that between the turret and the cannon. However, in my actual scenario, my camera is angled 60º, and the cannon has a slight rotation.

The following image illustrates the scenario: Illustrative Scenario

I'm not sure exactly why, but if I apply that same offset, it only seems to work while aiming at certain distances from the turret.

Is my logic flawed? Am I missing something fundamental here?

Final Edit: the solution provided by @JohnHamilton latest update solves this problem with perfect precision. I have now removed the code and images that I used to illustrate my incorrect implementations.

\$\endgroup\$
4
  • \$\begingroup\$ From a perspective of weapons design, you could just fix your gun ;) \$\endgroup\$ Dec 6, 2016 at 12:11
  • \$\begingroup\$ @WayneWerner this is not an option in my case. It's a design choice to have it be crooked, but functional. \$\endgroup\$ Dec 7, 2016 at 23:08
  • 1
    \$\begingroup\$ I've added a working example to my answer. \$\endgroup\$
    – ens
    Dec 10, 2016 at 21:07
  • \$\begingroup\$ It seems answers are perfect... can you mention what detail do you need exactly? \$\endgroup\$ Mar 4, 2018 at 6:57

4 Answers 4

35
\$\begingroup\$

The answer is actually pretty easy if you do the math. You have a fixed distance of Y and a variable distance of X (See Picture 1). You need to find out the angle between Z and X and turn your turret that much more. enter image description here

Step 1 - Get distance between the turret line (V) and the gun line (W) which is Y (this is constant but doesn't hurt to calculate). Get distance from the turret to the target (which is X).

Step 2 - Divide Y by X and then get the inverse sine of the value to recover the angle

double turnRadians = Mathf.Asin(Y/X);
double angle = Mathf.Rad2Deg * turnRadians;

//where B is the red dot, A is a point on the X line and C is a point on the Z line.

Step 3 - Turn the turret that much more (around the axis that goes from it's top to it's bottom, most likely up axis but only you can know that part).

gameObject.transform.Rotate(Vector3.up, turnAngle);

Of course in this case, you need it to turn counterclockwise so you might need to add a minus in front of the turnAngle there, as in -turnAngle.

Edited some parts. Thanks to @ens for pointing out the difference in distance.

The OP said his gun has an angle so here we go, image first, explanation later: enter image description here

We already know from the previous calculation where to aim the red line according to the blue line. So aiming for the blue line first:

float turnAngle = angleBetweenTurretAndTarget - angleBetweenTurretAndGun;
turret.transform.Rotate(Vector3.up, turnAngle);

The only calculation that differs here, is the calculation of "X Prime" (X') because the angle between the gun and the turret (angle "a") changed the distance between the lines.

//(this part had a mistake of using previous code inside new variable names, YPrime and Y are shown as X' and X in the 2nd picture.
float YPrime = Cos(a)*Y; //this part is what @ens is doing in his answer
double turnRadians = Mathf.Asin(YPrime/X);
double angle = Mathf.Rad2Deg * turnRadians;
turret.transform.Rotate(Vector3.up, angle);

This next part is ONLY necessary if you're doing the turret guns modular (i.e. user can change the guns on a turret and different guns have different angles). If you're doing this in editor, you can already see what the gun angle is according to the turret.

There are two methods for finding the angle "a" , one is the transform.up method:

float angleBetween = Vector3.Angle(turret.transform.up, gun.transform.up);

Above technique will calculate in 3D, so if you want a 2D result, you need to get rid of the Z axis (that's what I assume where gravity is, but if you changed nothing, in Unity it's Y axis that is up or down, i.e. gravity is on the Y axis, so you might have to change things up):

Vector2 turretVector = new Vector2(turret.transform.up.x, turret.transform.up.y);
Vector2 gunVector = new Vector2(gun.transform.up.x, gun.transform.up.y);
float angleBetween = Vector2.Angle(turretVector, gunVector);

Second way is the rotation method (I'm thinking in 2D in this case):

double angleRadians = Mathf.Asin(turret.transform.rotation.z - gun.transform.rotation.z);
double angle = 2 * Mathf.Rad2Deg * angleRadians;

Again, all these codes will give you values that are positive, so you might have to add or subtract the amount depending on the angle (there are calculations for that too, but I'm not going to go that in-depth). A good place to start on this would be the Vector2.Dotmethod in Unity.

Final block of code for additional explanation of what we're doing:

//turn turret towards target
turretTransform.up = targetTransform.position - turretTransform.position;
//adjust for gun angle
if (weaponTransform.localEulerAngles.z <180) //if the value is over 180 it's actually a negative for us
    turretTransform.Rotate(Vector3.forward, 90 - b - a);
else
    turretTransform.Rotate(Vector3.forward, 90 - b + a);

If you did everything right, you should get a scene like this (link for the unitypackage): enter image description here What I mean by always positive values:enter image description here

The Z method can give negative values:enter image description here

For an example scene, get the unitypackage from this link.

Here's the code I've used in the scene (on the turret):

public class TurretAimCorrection : MonoBehaviour
{
    public Transform targetTransform;
    public Transform turretTransform;
    public Transform weaponTransform;

    private float f, d, x, y, h, b, a, weaponAngle, turnAngle;
    private void Start()
    {
        TurnCorrection();
    }

    private void Update()
    {
        TurnCorrection();
    }
    void TurnCorrection()
    {
        //find distances and angles
        d = Vector2.Distance(new Vector2(targetTransform.position.x, targetTransform.position.y), new Vector2(turretTransform.position.x, turretTransform.position.y));
        x = Vector2.Distance(new Vector2(turretTransform.position.x, turretTransform.position.y), new Vector2(weaponTransform.position.x, weaponTransform.position.y));
        weaponAngle = weaponTransform.localEulerAngles.z;
        weaponAngle = weaponAngle * Mathf.Deg2Rad;
        y = Mathf.Abs(Mathf.Cos(weaponAngle) * x);
        b = Mathf.Rad2Deg * Mathf.Acos(y / d);
        a = Mathf.Rad2Deg * Mathf.Acos(y / x);
        //turn turret towards target
        turretTransform.up = targetTransform.position - turretTransform.position;
        //adjust for gun angle
        if (weaponTransform.localEulerAngles.z < 180)
            turretTransform.Rotate(Vector3.forward, 90 - b - a);
        else
            turretTransform.Rotate(Vector3.forward, 90 - b + a);
        //Please leave this comment in the code. This code was made by 
        //http://gamedev.stackexchange.com/users/93538/john-hamilton a.k.a. CrazyIvanTR. 
        //This code is provided as is, with no guarantees. It has worked in local tests on Unity 5.5.0f3.
    }
}

3D adapted code with X and Z as the 2D plane:

public class TurretAimCorrection : MonoBehaviour
{
    public Transform targetTransform; //drag target here
    public Transform turretTransform; //drag turret base or turret top part here
    public Transform weaponTransform; //drag the attached weapon here

    private float d, x, y, b, a, weaponAngle, turnAngle;
    private void Start()
    {
        TurnAdjustment();
    }

    private void Update()
    {
        TurnAdjustment();
    }
    void TurnAdjustment()
    {

        d = Vector2.Distance(new Vector2(targetTransform.position.x, targetTransform.position.z), new Vector2(turretTransform.position.x, turretTransform.position.z));
        x = Vector2.Distance(new Vector2(turretTransform.position.x, turretTransform.position.z), new Vector2(weaponTransform.position.x, weaponTransform.position.z));
        weaponAngle = weaponTransform.localEulerAngles.y;
        weaponAngle = weaponAngle * Mathf.Deg2Rad;
        y = Mathf.Abs(Mathf.Cos(weaponAngle) * x);
        b = Mathf.Rad2Deg * Mathf.Acos(y / d);
        a = Mathf.Rad2Deg * Mathf.Acos(y / x);
        //turn turret towards target
        turretTransform.forward = new Vector3(targetTransform.position.x, 0, targetTransform.position.z) - new Vector3(turretTransform.position.x, 0, turretTransform.position.z);
        //adjust for gun angle
        if (weaponTransform.localEulerAngles.y < 180)
            turretTransform.Rotate(Vector3.up, - a +b-90);
        else
            turretTransform.Rotate(Vector3.up, + a+ b - 90);
        //Please leave this comment in the code. This code was made by 
        //http://gamedev.stackexchange.com/users/93538/john-hamilton a.k.a. CrazyIvanTR. 
        //This code is provided as is, with no guarantees. It has worked in local tests on Unity 5.5.0f3.
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ There's a slight flaw in the first image. Z is the length of the turret to the box. X is the length of the turret to the box after rotation... x = z. Therefore, unless y is the hypotenuse that isn't a right triangle and sin does not apply. \$\endgroup\$
    – user64742
    Dec 6, 2016 at 7:56
  • \$\begingroup\$ @TheGreatDuck Z isn't the distance between turret and the box it's the Vector2.forward of that turret (it's just shown finite instead of having an arrow at the end). Even if Z was the distance, the picture has units and you can see that Z < X without even calculating. \$\endgroup\$ Dec 6, 2016 at 8:57
  • 2
    \$\begingroup\$ @Franconstein you don't have to first turn the turret, then apply these. You can first calculate these, then add the degree that you get from these equations to the turret's turn degree. (So instead of turning the turret 20 degrees to the object, then adjusting for the gun, you would be turning the turret by 20 degrees + adjustment for the gun). \$\endgroup\$ Dec 6, 2016 at 20:48
  • \$\begingroup\$ @Franconstein See the newly adjusted code. Since we changed planes the code was acting differently than it was in the other version. I have no idea why this happened but it's working perfectly on my end now. See: imgur.com/a/1scEH (removing your turrets was not necessary, those simple models acted the same way as yours did). \$\endgroup\$ Dec 10, 2016 at 18:39
  • 1
    \$\begingroup\$ @JohnHamilton You did it! It is finally resolved, and with laser-like precision, too! Thank you! Thank you! Thank you! I will now edit my post to how it was in the beginning, so it can be more easily understood for future reference! Once more, thank you! \$\endgroup\$ Dec 10, 2016 at 19:49
3
\$\begingroup\$

You could also use a more general approach:

The math for your problem already exists in the form of the scalarproduct (or dot product). You only need to get the directions of your weapons forward axis and the direction from your weapon to the target.

Let W be your weapon's forward vector.

Let D be the direction from your weapon to your target. (Target.pos - Weapon.pos)

If you solve the dot product's formula

dot(A,B) = |A|*|B|*cos(alpha) with alpha = the angle between A and B

for alpha, you get:

              ( dot(W,D) )
alpha = arccos(--------- )
              ( |W|*|D|  )

You only have to convert radians to degrees and you got your angle to rotate your robot. (As you mentioned the weapon is at an angle to your robot, so you need to add the angle to alpha)

enter image description hereenter image description here

\$\endgroup\$
2
+100
\$\begingroup\$

All the answers posted so far are (more or less) wrong, so here's a quick correct solution:

enter image description here

To aim the gun towards the target, rotate the turret forward vector to the target and add the angle θ.

So let's find θ:

   d / sin(δ) = a / sin(α) # By the Law of Sines
=> α = asin(a * sin(δ) / d)

   β = 180° - α - δ
   θ = 90° - β
=> θ = α + δ - 90°
     = asin(a * sin(δ) / d) + δ - 90°
     = asin(a * cos(δ') / d) - δ' # Where (δ' = 90° - δ) is the angle between 
                                  # the gun and the turret forward vector.

When δ' = 0 this simplifies to θ = asin(a / d), which matches the first part of John Hamilton's answer.

Edit:

I've added a working example.

Open in JSFiddle or use the embedded snippet below:

var Degree = Math.PI / 180;
var showDebugOverlays = false;

var Turret = {
    gunAngle: -10 * Degree,
    maxGunAngle: 85 * Degree,
    adaptedGunXOffset: null,

    rotateToTarget: function (target) {
        var delta = Vec.subtract(this.position, target.position);
        var dist = Vec.length(delta);
        var angle = Vec.angle(delta);

        theta = Math.asin(this.adaptedGunXOffset / dist) + this.gunAngle;
        this.rotation = -(angle + theta);

        this.updateGunRay(target);
    },

    setGunAngle: function (angle) {
        var angle = this.clampGunAngle(angle);
        this.gunAngle = angle;
        this.gun.rotation = angle;
        // Account for the fact that the origin of the gun also has an y offset
        // relative to the turret origin
        var extraXOffset = this.gun.position.y * Math.tan(angle);
        var gunXOffset = this.gun.position.x + extraXOffset;
        // This equals "a * cos(δ')" in the angle formula
        this.adaptedGunXOffset = gunXOffset * Math.cos(-angle);

        if (showDebugOverlays) {
            // Show x offsets
            this.removeChild(this.xOffsetOverlay);
            this.removeChild(this.extraXOffsetOverlay);
            this.xOffsetOverlay = addRect(this, 0, 0, this.gun.position.x, 1, 0xf6ff00);
            this.extraXOffsetOverlay = addRect(this, this.gun.position.x, 0, extraXOffset, 1, 0xff00ae);
        }
    },

    rotateGun: function (angleDelta) {
        this.setGunAngle(this.gunAngle + angleDelta);
    },

    updateGunRay: function (target) {
        var delta = this.gun.toLocal(target.position);
        var dist = Vec.length(delta);
        this.gun.removeChild(this.gun.ray);
        this.gun.ray = makeLine(0, 0, 0, -dist);
        this.gun.addChildAt(this.gun.ray, 0);
    },

    clampGunAngle: function (angle) {
        if (angle > this.maxGunAngle) {
            return this.maxGunAngle;
        }
        if (angle < -this.maxGunAngle) {
            return -this.maxGunAngle;
        }
        return angle;
    }
}

function makeTurret() {
    var turret = new PIXI.Sprite.fromImage('http://i.imgur.com/gPtlPJh.png');
    var gunPos = new PIXI.Point(25, -25)

    turret.anchor.set(0.5, 0.5);

    var gun = new PIXI.Container();
    var gunImg = new PIXI.Sprite.fromImage('http://i.imgur.com/RE45GEY.png');
    gun.ray = makeLine(0, 0, 0, -250);
    gun.addChild(gun.ray);

    gunImg.anchor.set(0.5, 0.6);
    gun.addChild(gunImg);
    gun.position = gunPos;

    // Turret forward vector
    turret.addChild(makeLine(0, -38, 0, -90, 0x38ce2c));
    turret.addChild(gun);
    turret.gun = gun;

    Object.setPrototypeOf(Turret, Object.getPrototypeOf(turret));
    Object.setPrototypeOf(turret, Turret);

    turret.setGunAngle(turret.gunAngle);

    if (showDebugOverlays) {
        addRect(turret, 0, 0, 1, 1); // Show turret origin
        addRect(gun, -1, 1, 2, 2, 0xff0096); // Show gun origin
    }

    return turret;
}

function makeTarget() {
    var target = new PIXI.Graphics();
    target.beginFill(0xd92f8f);
    target.drawCircle(0, 0, 9);
    target.endFill();
    return target;
}

var CursorKeys = {
    map: { ArrowLeft: -1, ArrowRight: 1 },
    pressedKeyDirection: null,

    onKeyDown: function (keyEvent) {
        var key = this.map[keyEvent.key];
        if (key) {
            this.pressedKeyDirection = key;
        }
    },
    onKeyUp: function (keyEvent) {
        var key = this.map[keyEvent.key];
        if (key) {
            if (this.pressedKeyDirection == key) {
                this.pressedKeyDirection = null;
            }
        }
    }
}

document.body.addEventListener("keydown", CursorKeys.onKeyDown.bind(CursorKeys));
document.body.addEventListener("keyup", CursorKeys.onKeyUp.bind(CursorKeys));

function makeLine(x1, y1, x2, y2, color) {
    if (color == undefined) {
        color = 0x66CCFF;
    }
    var line = new PIXI.Graphics();
    line.lineStyle(1.5, color, 1);
    line.moveTo(x1, y1);
    line.lineTo(x2, y2);
    return line;
}

function addRect(parent, x, y, w, h, color) {
    if (color == undefined) {
        color = 0x66CCFF;
    }
    var rectangle = new PIXI.Graphics();
    rectangle.beginFill(color);
    rectangle.drawRect(x, y, w, h);
    rectangle.endFill();
    parent.addChild(rectangle);
    return rectangle;
}

var Vec = {
    subtract: function (a, b) {
        return { x: a.x - b.x,
                 y: a.y - b.y };
    },
    length: function (v) {
        return Math.sqrt(v.x * v.x + v.y * v.y);
    },
    angle: function (v) {
        return Math.atan2(v.x, v.y)
    }
}

Math.clamp = function(n, min, max) {
    return Math.max(min, Math.min(n, max));
}

var renderer;
var stage;
var turret;
var target;

function run() {
    renderer = PIXI.autoDetectRenderer(600, 300, { antialias: true });
    renderer.backgroundColor = 0x2a2f34;
    document.body.appendChild(renderer.view);
    stage = new PIXI.Container();
    stage.interactive = true;

    target = makeTarget();
    target.position = { x: renderer.width * 0.2, y: renderer.height * 0.3 };

    turret = makeTurret();
    turret.position = { x: renderer.width * 0.65, y: renderer.height * 0.3 };
    turret.rotateToTarget(target);

    stage.addChild(turret);
    stage.addChild(target);

    var message = new PIXI.Text(
        "Controls: Mouse, left/right cursor keys",
        {font: "18px Arial", fill: "#7c7c7c"}
    );
    message.position.set(10, 10);
    stage.addChild(message);

    stage.on('mousemove', function(e) {
        var pos = e.data.global;
        target.position.x = Math.clamp(pos.x, 0, renderer.width);
        target.position.y = Math.clamp(pos.y, 0, renderer.height);
        turret.rotateToTarget(target);
    })

    animate();
}

function animate() {
    requestAnimationFrame(animate);

    if (CursorKeys.pressedKeyDirection) {
        turret.rotateGun(3 * Degree * CursorKeys.pressedKeyDirection);
        turret.rotateToTarget(target);
    }

    renderer.render(stage);
}

run();
body {
    padding: 0;
    margin: 0;
}

#capture_focus {
  position: absolute;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.2.2/pixi.min.js"></script>
<div id="capture_focus" />

\$\endgroup\$
4
  • \$\begingroup\$ Thank you very much for this explanation. It was simple enough for me to understand, and it seems to take into account every situation. However, when I implemented it, the results I obtained where not favorable. I have edited my original post to include my code, an image visualizing my setup, and the results for each variable. My turret's forward vector is always looking at the target, but even if it's not, the results remain almost the same. Am I doing something wrong? Is it my code? \$\endgroup\$ Dec 8, 2016 at 20:48
  • \$\begingroup\$ If the other answers "are more or less wrong", you do not understand / implement them correctly. I've used both alternate answers, previously, to create the desired behavior. @Franconstein, I even see your comments on atleast one to say you have verified that it works. If you've verified a solution, do you still have a problem? \$\endgroup\$
    – Gnemlock
    Dec 8, 2016 at 21:22
  • \$\begingroup\$ @Gnemlock, John Hamilton's solution was not wrong - I implemented it, and it worked, and thus I verified his solution as approved. But after implementing it, I started trying different non-static scenarios, and the solution did not hold up. I didn't want to prematurely discard it, though, so I went over it with a colleague. We ended up confirming it doesn't hold, but now ens posted another possible solution, and John edited his post to include it. As of this moment, I cannot confirm either of them work correctly, and am still attempting. I posted my code to see if it helps. Did I do wrong? \$\endgroup\$ Dec 8, 2016 at 21:35
  • \$\begingroup\$ @Franconstein, in this form it is overly confusing. I would say this example is a fine example of what you would expect reading a Maths text book, but it is overbearingly confusing in relation to general game programming. The only important element is the angle (which the original answer John Hamilton posted did provide). I see what you mean by particular angles, ultimately you may have done this incorrectly. I find there is a lot of room, in this answer, to do it incorrectly. \$\endgroup\$
    – Gnemlock
    Dec 8, 2016 at 21:49
0
\$\begingroup\$

Rotate the turret base in a simple manner, except subtract the "fire from" offset position to your target position. Roughly:

    Vector3 adjustedTargetPos = TargetPos - (gunTipPos - TurretBaseTransform.position);
    Quaternion adjustedRotToTarget = Quaternion.LookRotation(adjustedTargetPos - TurretBaseTransform.position);
    TurretBaseTransform.rotation = adjustedRotToTarget;

This is a lot simpler than other methods I have used and seems to work great. Tested with 3d turret and 3D IK aiming. 2D or gun tip rotation not taken into account but should be easy. I dunno, am I missing something or does this work like a charm for others?

\$\endgroup\$
4
  • \$\begingroup\$ Keep in mind that the offset vector between the gun tip and the turret base, in world space, also changes as you rotate. If you find this is working, it's probably that you're running it every frame - so rotational error from the previous frame is reduced by re-running it the next frame, and you quickly converge on the correct rotation. Other methods show how to compute the rotation in a single step, say if you wanted to tween to a target rotation you compute just once, rather than re-calculating your rotation every frame. \$\endgroup\$
    – DMGregory
    Oct 28, 2022 at 21:56
  • \$\begingroup\$ You are absolutely correct. My usage has rotations updated every frame. It works surprisingly well and is pinpoint accurate. However, if I set the rotation speed really large then I can see it overshoot the target for a frame or two - showing the error you mention. \$\endgroup\$
    – Foulcloud
    Oct 29, 2022 at 20:04
  • \$\begingroup\$ John Hamilton's "Adapted 3D Code" works well on a 3D turret, but only rotates in 2D. Any ideas on how to adapt that so it addresses all 3 dimensions? I need elevation and that is not accounted for. Required for turret that rotates in 3D or aiming a hand on a wrist. I tried to comment to ask on his answer but I have no reputation, ha! Thanks DMGregory for your insight. \$\endgroup\$
    – Foulcloud
    Oct 30, 2022 at 1:36
  • \$\begingroup\$ You can click the "Ask Question" button to ask for 3D solutions to this problem, if you can only find 2D solutions here. \$\endgroup\$
    – DMGregory
    Oct 30, 2022 at 1:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .