Come posso ruotare un oggetto basato sull'offset di un altro rispetto ad esso?


25

Ho un modello 3D di una torretta che ruota attorno all'asse Y. Questa torretta ha un cannone che è significativamente fuori dal centro dell'oggetto. Voglio che il cannone, non la torretta, miri a un bersaglio specifico. Posso solo ruotare la torretta, tuttavia, e quindi non so quale equazione devo applicare per raggiungere l'obiettivo.

L'immagine seguente illustra il mio problema:inserisci qui la descrizione dell'immagine

Se ho la torretta "LookAt ()" il bersaglio, un laser proveniente dal cannone mancherà completamente detto bersaglio.

Se questo fosse uno scenario completamente dall'alto verso il basso e il cannone fosse esattamente parallelo alla torretta, la mia logica mi dice che il bersaglio falso dovrebbe trovarsi in una posizione uguale al bersaglio reale più un offset uguale a quello tra il torretta e il cannone. Tuttavia, nel mio scenario reale, la mia fotocamera è inclinata di 60º e il cannone ha una leggera rotazione.

L'immagine seguente mostra lo scenario: Scenario illustrativo

Non sono sicuro del perché, ma se applico lo stesso offset, sembra funzionare solo mirando a determinate distanze dalla torretta.

La mia logica è difettosa? Mi sto perdendo qualcosa di fondamentale qui?

Modifica finale: la soluzione fornita dall'ultimo aggiornamento di @JohnHamilton risolve questo problema con una precisione perfetta. Ho rimosso il codice e le immagini che ho usato per illustrare le mie implementazioni errate.


Dal punto di vista del design delle armi, potresti semplicemente riparare la tua pistola ;)
Wayne Werner il

@WayneWerner questa non è un'opzione nel mio caso. È una scelta progettuale per averlo storto, ma funzionale.
Franconstein,

1
Ho aggiunto un esempio funzionante alla mia risposta .
ens

Sembra che le risposte siano perfette ... puoi dire quali dettagli hai bisogno esattamente?
Seyed Morteza Kamali

Risposte:


31

La risposta è in realtà abbastanza semplice se fai la matematica. Hai una distanza fissa di Y e una distanza variabile di X (vedi figura 1). Devi scoprire l'angolo tra Z e X e girare la torretta molto di più. inserisci qui la descrizione dell'immagine

Passaggio 1: ottenere la distanza tra la linea della torretta (V) e la linea della pistola (W) che è Y (questo è costante ma non fa male per il calcolo). Prendi la distanza dalla torretta al bersaglio (che è X).

Passaggio 2: Dividi Y per X e ottieni il seno iperbolico del valore

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.

Passaggio 3: ruota la torretta molto di più (attorno all'asse che va dall'alto verso il basso, molto probabilmente verso l'alto ma solo tu puoi conoscere quella parte).

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

Ovviamente in questo caso, è necessario che giri in senso antiorario, quindi potrebbe essere necessario aggiungere un segno negativo di fronte al turno, come in -turnAngle.

Ho modificato alcune parti. Grazie a @ens per aver sottolineato la differenza di distanza.

L'OP ha detto che la sua pistola ha un angolo, quindi eccoci qui, prima l'immagine, la spiegazione più tardi: inserisci qui la descrizione dell'immagine

Sappiamo già dal calcolo precedente dove puntare la linea rossa secondo la linea blu. Quindi puntando prima alla linea blu:

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

L'unico calcolo che qui differisce, è il calcolo di "X Prime" (X ') perché l'angolo tra la pistola e la torretta (angolo "a") ha cambiato la distanza tra le linee.

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

Questa parte successiva è SOLO necessaria se stai facendo le pistole a torretta modulari (cioè l'utente può cambiare le pistole su una torretta e pistole diverse hanno angoli diversi). Se lo stai facendo nell'editor, puoi già vedere qual è l'angolo della pistola in base alla torretta.

Esistono due metodi per trovare l'angolo "a", uno è il metodo transform.up:

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

La tecnica sopra calcolerà in 3D, quindi se vuoi un risultato 2D, devi sbarazzarti dell'asse Z (è quello che presumo dov'è la gravità, ma se non hai cambiato nulla, in Unity è l'asse Y che è su o giù, cioè la gravità è sull'asse Y, quindi potresti dover cambiare le cose):

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

Il secondo modo è il metodo di rotazione (sto pensando in 2D in questo caso):

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

Ancora una volta, tutti questi codici ti daranno valori positivi, quindi potresti dover aggiungere o sottrarre la quantità in base all'angolo (ci sono anche calcoli per quello, ma non ho intenzione di approfondirlo). Un buon punto di partenza sarebbe il Vector2.Dotmetodo in Unity.

Blocco finale di codice per una spiegazione aggiuntiva di ciò che stiamo facendo:

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

Se hai fatto tutto nel modo giusto, dovresti ottenere una scena come questa ( link per il pacchetto di unità ): inserisci qui la descrizione dell'immagine cosa intendo per valori sempre positivi:inserisci qui la descrizione dell'immagine

Il metodo Z può dare valori negativi:inserisci qui la descrizione dell'immagine

Per una scena di esempio, ottieni il pacchetto di unità da questo link .

Ecco il codice che ho usato nella scena (sulla torretta):

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

Codice adattato 3D con X e Z come piano 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.
    }
}

C'è un piccolo difetto nella prima immagine. Z è la lunghezza della torretta rispetto alla scatola. X è la lunghezza della torretta rispetto alla scatola dopo la rotazione ... x = z. Pertanto, a meno che y non sia l'ipotenusa che non è un triangolo rettangolo e il peccato non si applica.
The Great Duck il

@TheGreatDuck Z non è la distanza tra la torretta e la scatola, è il Vector2.forward di quella torretta (è solo mostrato finito invece di avere una freccia alla fine). Anche se Z fosse la distanza, l'immagine ha unità e puoi vedere quella Z <X senza nemmeno calcolarla.
John Hamilton,

2
@Franconstein non devi prima girare la torretta, quindi applicare questi. Puoi prima calcolarli, quindi aggiungere il grado che ottieni da queste equazioni al grado di rotazione della torretta. (Quindi, invece di girare la torretta di 20 gradi rispetto all'oggetto, quindi regolando per la pistola, gireresti la torretta di 20 gradi + regolazione per la pistola).
John Hamilton,

@Franconstein Vedi il codice appena modificato. Da quando abbiamo cambiato aereo, il codice ha funzionato in modo diverso rispetto all'altra versione. Non ho idea del perché sia ​​successo, ma ora sta funzionando perfettamente. Vedi: imgur.com/a/1scEH (non era necessario rimuovere le torrette, quei semplici modelli si comportavano come i tuoi).
John Hamilton,

1
@JohnHamilton L'hai fatto! Viene finalmente risolto, e anche con precisione simile al laser! Grazie! Grazie! Grazie! Ora modificherò il mio post com'era all'inizio, in modo che possa essere più facilmente compreso per riferimento futuro! Ancora una volta, grazie!
Franconstein,

3

È inoltre possibile utilizzare un approccio più generale:

La matematica per il tuo problema esiste già sotto forma del prodotto scalare (o prodotto punto) . Hai solo bisogno di ottenere le direzioni delle tue armi avanti asse e la direzione dalla tua arma al bersaglio.

Lascia che W sia il vettore in avanti della tua arma.

Lascia che D sia la direzione dalla tua arma al bersaglio. (Target.pos - Weapon.pos)

Se risolvi la formula del prodotto punto

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

per alpha, ottieni:

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

Devi solo convertire i radianti in gradi e hai l'angolazione per ruotare il tuo robot. (Come hai detto, l'arma è inclinata rispetto al robot, quindi è necessario aggiungere l'angolo all'alfa)

inserisci qui la descrizione dell'immagineinserisci qui la descrizione dell'immagine


2

Tutte le risposte pubblicate finora sono (più o meno) sbagliate, quindi ecco una soluzione rapida e corretta:

inserisci qui la descrizione dell'immagine

Per puntare la pistola verso il bersaglio, ruotare il vettore in avanti della torretta verso il bersaglio e aggiungere l'angolo θ.

Quindi troviamo θ:

   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.

Quando δ' = 0questo si semplifica θ = asin(a / d), che corrisponde alla prima parte della risposta di John Hamilton.

Modificare:

Ho aggiunto un esempio funzionante.

Apri in JSFiddle o utilizza lo snippet incorporato di seguito:

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


Grazie mille per questa spiegazione. È stato abbastanza semplice per me capire e sembra prendere in considerazione ogni situazione. Tuttavia, quando l'ho implementato, i risultati ottenuti non erano favorevoli. Ho modificato il mio post originale per includere il mio codice, un'immagine che visualizza la mia configurazione e i risultati per ogni variabile. Il vettore in avanti della mia torretta guarda sempre il bersaglio, ma anche se non lo è, i risultati rimangono quasi gli stessi. Sto facendo qualcosa di sbagliato? È il mio codice?
Franconstein,

Se le altre risposte "sono più o meno sbagliate", non le capisci / le implementi correttamente. Ho usato entrambe le risposte alternative, in precedenza, per creare il comportamento desiderato. @Franconstein, ho anche visto i tuoi commenti su almeno uno per dire che hai verificato che funziona. Se hai verificato una soluzione, hai ancora problemi?
Gnemlock,

@Gnemlock, la soluzione di John Hamilton non era sbagliata: l'ho implementata e ha funzionato, quindi ho verificato la sua soluzione approvata. Ma dopo averlo implementato, ho iniziato a provare diversi scenari non statici e la soluzione non ha retto. Tuttavia, non volevo scartarlo prematuramente, quindi l'ho esaminato con un collega. Abbiamo finito per confermare che non regge, ma ora abbiamo pubblicato un'altra possibile soluzione e John ha modificato il suo post per includerlo. A partire da questo momento, non posso confermare che nessuno dei due funzioni correttamente e sto ancora tentando. Ho pubblicato il mio codice per vedere se aiuta. Ho sbagliato?
Franconstein,

@Franconstein, in questa forma è troppo confuso. Direi che questo esempio è un ottimo esempio di ciò che ti aspetteresti di leggere un libro di matematica , ma è eccessivamente confuso in relazione alla programmazione generale del gioco. L'unico elemento importante è l' angolo (fornito dalla risposta originale pubblicata da John Hamilton). Capisco cosa intendi per angoli particolari, in ultima analisi, si può aver fatto questo in modo non corretto. Trovo che ci sia molto spazio, in questa risposta, per farlo in modo errato .
Gnemlock,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.