Comment puis-je faire pivoter un objet en fonction du décalage d'un autre par rapport à lui?

25

J'ai un modèle 3D d'une tourelle qui tourne autour de l'axe Y. Cette tourelle a un canon qui est sensiblement hors du centre de l'objet. Je veux que le canon, pas la tourelle, vise une cible spécifiée. Je ne peux que faire pivoter la tourelle, cependant, et donc je ne sais pas quelle équation je dois appliquer pour accomplir par objectif.

L'image suivante illustre mon problème:entrez la description de l'image ici

Si j'ai la tourelle "LookAt ()" la cible, un laser provenant du canon manquera complètement ladite cible.

S'il s'agissait d'un scénario complètement descendant et que le canon était exactement parallèle à la tourelle, ma logique me dit que la fausse cible devrait être située à une position égale à la cible réelle plus un décalage égal à celui entre le tourelle et le canon. Cependant, dans mon scénario actuel, ma caméra est inclinée à 60 ° et le canon a une légère rotation.

L'image suivante illustre le scénario: Scénario illustratif

Je ne sais pas exactement pourquoi, mais si j'applique le même décalage, cela ne semble fonctionner que lorsque l'on vise certaines distances de la tourelle.

Ma logique est-elle défectueuse? Suis-je en train de manquer quelque chose de fondamental ici?

Final Edit: la solution fournie par la dernière mise à jour de @JohnHamilton résout ce problème avec une précision parfaite. J'ai maintenant supprimé le code et les images que j'ai utilisés pour illustrer mes implémentations incorrectes.

Franconstein
la source
Du point de vue de la conception des armes, vous pouvez simplement réparer votre arme ;)
Wayne Werner
@WayneWerner ce n'est pas une option dans mon cas. C'est un choix de conception pour qu'il soit tordu, mais fonctionnel.
Franconstein
1
J'ai ajouté un exemple de travail à ma réponse .
ens
Il semble que les réponses soient parfaites ... pouvez-vous mentionner de quels détails vous avez besoin exactement?
Seyed Morteza Kamali

Réponses:

31

La réponse est en fait assez simple si vous faites le calcul. Vous avez une distance fixe de Y et une distance variable de X (voir image 1). Vous devez trouver l'angle entre Z et X et tourner encore plus votre tourelle. entrez la description de l'image ici

Étape 1 - Obtenez la distance entre la ligne de tourelle (V) et la ligne de canon (W) qui est Y (c'est constant mais cela ne fait pas de mal à calculer). Obtenez la distance de la tourelle à la cible (qui est X).

Étape 2 - Divisez Y par X, puis obtenez le sinus hyperbolique de la valeur

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.

Étape 3 - Tournez beaucoup plus la tourelle (autour de l'axe qui va de haut en bas, probablement vers le haut, mais vous seul pouvez connaître cette partie).

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

Bien sûr, dans ce cas, vous en avez besoin pour tourner dans le sens antihoraire, vous devrez peut-être ajouter un moins devant le turnAngle là, comme dans -turnAngle .

Modifié certaines parties. Merci à @ens d'avoir souligné la différence de distance.

L'OP a déclaré que son arme avait un angle alors nous y allons, l'image d'abord, l'explication plus tard: entrez la description de l'image ici

Nous savons déjà du calcul précédent où viser la ligne rouge en fonction de la ligne bleue. Donc, visons d'abord la ligne bleue:

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

Le seul calcul qui diffère ici est le calcul de "X Prime" (X ') parce que l'angle entre le canon et la tourelle (angle "a") a changé la distance entre les lignes.

//(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);

Cette partie suivante est UNIQUEMENT nécessaire si vous faites des pistolets à tourelle modulaires (c.-à-d. L'utilisateur peut changer les pistolets sur une tourelle et différents pistolets ont des angles différents). Si vous faites cela dans l'éditeur, vous pouvez déjà voir quel est l'angle du canon selon la tourelle.

Il existe deux méthodes pour trouver l'angle "a", l'une est la méthode transform.up:

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

La technique ci-dessus calculera en 3D, donc si vous voulez un résultat 2D, vous devez vous débarrasser de l'axe Z (c'est ce que je suppose où se trouve la gravité, mais si vous n'avez rien changé, dans Unity, c'est l'axe Y qui est vers le haut ou vers le bas, c'est-à-dire que la gravité est sur l'axe Y, vous devrez donc peut-être changer les choses):

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);

La deuxième façon est la méthode de rotation (je pense en 2D dans ce cas):

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

Encore une fois, tous ces codes vous donneront des valeurs positives, vous devrez donc peut-être ajouter ou soustraire le montant en fonction de l'angle (il y a des calculs pour cela aussi, mais je ne vais pas aller plus loin). Un bon point de départ serait leVector2.Dot méthode dans Unity.

Dernier bloc de code pour une explication supplémentaire de ce que nous faisons:

//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);

Si vous avez tout fait correctement, vous devriez obtenir une scène comme celle-ci ( lien pour le package d'unité ): entrez la description de l'image ici Ce que je veux dire par des valeurs toujours positives:entrez la description de l'image ici

La méthode Z peut donner des valeurs négatives:entrez la description de l'image ici

Pour un exemple de scène, obtenez le package d'unité à partir de ce lien .

Voici le code que j'ai utilisé dans la scène (sur la tourelle):

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.
    }
}

Code adapté en 3D avec X et Z comme plan 2D:

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.
    }
}
John Hamilton
la source
Il y a un léger défaut dans la première image. Z est la longueur de la tourelle à la boîte. X est la longueur de la tourelle à la boîte après rotation ... x = z. Par conséquent, à moins que y ne soit l'hypoténuse, ce n'est pas un triangle rectangle et le péché ne s'applique pas.
The Great Duck
@TheGreatDuck Z n'est pas la distance entre la tourelle et la boîte, c'est le Vector2.forward de cette tourelle (il est juste montré fini au lieu d'avoir une flèche à la fin). Même si Z était la distance, l'image a des unités et vous pouvez voir que Z <X sans même calculer.
John Hamilton
2
@Franconstein vous n'avez pas à tourner d'abord la tourelle, puis à les appliquer. Vous pouvez d'abord les calculer, puis ajouter le degré que vous obtenez à partir de ces équations au degré de tour de la tourelle. (Donc, au lieu de tourner la tourelle de 20 degrés par rapport à l'objet, puis de régler pour le pistolet, vous tourneriez la tourelle de 20 degrés + réglage pour le pistolet).
John Hamilton
@Franconstein Voir le code nouvellement ajusté. Depuis que nous avons changé d'avion, le code agissait différemment de ce qu'il était dans l'autre version. Je ne sais pas pourquoi cela s'est produit, mais cela fonctionne parfaitement de mon côté maintenant. Voir: imgur.com/a/1scEH (retirer vos tourelles n'était pas nécessaire, ces modèles simples ont agi de la même manière que les vôtres).
John Hamilton
1
@JohnHamilton Vous l'avez fait! Il est enfin résolu, et avec une précision comparable à celle du laser! Merci! Merci! Merci! Je vais maintenant modifier mon article comme il était au début, afin qu'il puisse être plus facilement compris pour référence future! Encore une fois merci!
Franconstein
3

Vous pouvez également utiliser une approche plus générale:

Le calcul de votre problème existe déjà sous la forme du produit scalaire (ou produit scalaire) . Il vous suffit d'obtenir les directions de l'axe avant de vos armes et la direction de votre arme vers la cible.

Soit W le vecteur avant de votre arme.

Soit D la direction de votre arme vers votre cible. (Target.pos - Weapon.pos)

Si vous résolvez la formule du produit scalaire

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

pour alpha, vous obtenez:

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

Vous n'avez qu'à convertir des radians en degrés et vous avez obtenu votre angle pour faire tourner votre robot. (Comme vous l'avez mentionné, l'arme est inclinée par rapport à votre robot, vous devez donc ajouter l'angle à alpha)

entrez la description de l'image icientrez la description de l'image ici

rootmenu
la source
2

Toutes les réponses publiées jusqu'à présent sont (plus ou moins) fausses, alors voici une solution rapide et correcte:

entrez la description de l'image ici

Pour viser le canon vers la cible, tournez le vecteur de la tourelle vers l'avant vers la cible et ajoutez l'angle θ.

Trouvons donc θ:

   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.

Lorsque δ' = 0cela se simplifie en θ = asin(a / d), ce qui correspond à la première partie de la réponse de John Hamilton.

Modifier:

J'ai ajouté un exemple de travail.

Ouvrez dans JSFiddle ou utilisez l'extrait de code intégré ci-dessous:

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" />

ens
la source
Merci beaucoup pour cette explication. C'était assez simple pour moi de comprendre, et il semble tenir compte de chaque situation. Cependant, lorsque je l'ai mis en œuvre, les résultats que j'ai obtenus n'étaient pas favorables. J'ai modifié mon article d'origine pour inclure mon code, une image visualisant ma configuration et les résultats pour chaque variable. Le vecteur avant de ma tourelle regarde toujours la cible, mais même si ce n'est pas le cas, les résultats restent presque les mêmes. Est-ce que je fais quelque chose de mal? C'est mon code?
Franconstein
Si les autres réponses "sont plus ou moins fausses", vous ne les comprenez pas / ne les implémentez pas correctement. J'ai utilisé les deux réponses alternatives, précédemment, pour créer le comportement souhaité. @Franconstein, je vois même vos commentaires sur au moins un pour dire que vous avez vérifié que cela fonctionne. Si vous avez vérifié une solution, avez-vous toujours un problème?
Gnemlock
@Gnemlock, la solution de John Hamilton n'était pas fausse - je l'ai mise en œuvre, et cela a fonctionné, et j'ai donc vérifié sa solution comme approuvée. Mais après l'avoir implémenté, j'ai commencé à essayer différents scénarios non statiques, et la solution n'a pas résisté. Je ne voulais pas le jeter prématurément, cependant, je l'ai donc examiné avec un collègue. Nous avons fini par confirmer que cela ne tenait pas, mais maintenant ens a publié une autre solution possible, et John a modifié son message pour l'inclure. À partir de ce moment, je ne peux pas confirmer que l'un ou l'autre fonctionne correctement et j'essaie toujours. J'ai publié mon code pour voir si cela aide. Ai-je mal fait?
Franconstein
@Franconstein, sous cette forme, c'est trop déroutant. Je dirais que cet exemple est un bel exemple de ce à quoi vous vous attendez en lisant un manuel de mathématiques , mais il est extrêmement déroutant par rapport à la programmation générale de jeux. Le seul élément important est l' angle (que la réponse originale publiée par John Hamilton a fourni). Je vois ce que vous entendez par des angles particuliers, finalement vous avez peut- être mal fait cela. Je trouve qu'il y a beaucoup de place, dans cette réponse, pour le faire incorrectement .
Gnemlock