Jak mogę obrócić obiekt na podstawie przesunięcia względem niego innego?


25

Mam model 3D wieży, która obraca się wokół osi Y. Ta wieża ma działo, które znajduje się znacznie poza środkiem obiektu. Chcę, aby armata, a nie wieża, celowała w określony cel. Mogę jednak tylko obracać wieżę i dlatego nie wiem, jakie równanie muszę zastosować, aby osiągnąć cel.

Poniższy obraz ilustruje mój problem:wprowadź opis zdjęcia tutaj

Jeśli mam cel „LookAt ()” na wieży, laser pochodzący z armaty całkowicie nie trafi w ten cel.

Jeśli byłby to całkowicie odgórny scenariusz, a armata była dokładnie równoległa do wieży, moja logika podpowiada mi, że fałszywy cel powinien znajdować się w pozycji, która jest równa rzeczywistemu celowi plus przesunięcie równe temu między wieżyczka i działo. Jednak w moim scenariuszu mój aparat jest ustawiony pod kątem 60º, a działo obraca się lekko.

Poniższy obraz ilustruje scenariusz: Scenariusz poglądowy

Nie jestem do końca pewien, dlaczego, ale jeśli zastosuję to samo przesunięcie, wydaje się, że działa ono tylko podczas celowania na pewne odległości od wieży.

Czy moja logika jest wadliwa? Czy brakuje mi czegoś fundamentalnego?

Ostateczna edycja: rozwiązanie dostarczone przez najnowszą aktualizację @JohnHamilton rozwiązuje ten problem z doskonałą precyzją. Usunąłem teraz kod i obrazy, których użyłem do zilustrowania moich niepoprawnych implementacji.


Z perspektywy projektowania broni można po prostu naprawić broń ;)
Wayne Werner

@WayneWerner to nie jest opcja w moim przypadku. Jest to wybór projektowy, aby być krzywy, ale funkcjonalny.
Franconstein,

1
Do mojej odpowiedzi dodałem działający przykład .
ens

Wygląda na to, że odpowiedzi są idealne ... czy możesz wspomnieć, jakich szczegółów potrzebujesz?
Seyed Morteza Kamali

Odpowiedzi:


31

Odpowiedź jest dość prosta, jeśli wykonasz matematykę. Masz ustaloną odległość Y i zmienną odległość X (patrz zdjęcie 1). Musisz znaleźć kąt między Z i X i jeszcze bardziej obrócić wieżę. wprowadź opis zdjęcia tutaj

Krok 1 - Uzyskaj odległość między linią wieży (V) a linią dział (W), która ma wartość Y (jest stała, ale nie boli, aby ją obliczyć). Uzyskaj odległość od wieży do celu (czyli X).

Krok 2 - Podziel Y przez X, a następnie uzyskaj sinus hiperboliczny wartości

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.

Krok 3 - Obróć wieżę jeszcze bardziej (wokół osi, która biegnie od góry do dołu, najprawdopodobniej w górę, ale tylko ty możesz poznać tę część).

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

Oczywiście w tym przypadku trzeba go obrócić przeciwnie do ruchu wskazówek zegara, więc może być konieczne dodanie minus przed zakrętem tam, jak w -turnAngle.

Edytowałem niektóre części. Dzięki @ens za wskazanie różnicy odległości.

OP powiedział, że jego pistolet ma kąt, więc proszę, najpierw zdjęcie, wyjaśnienie później: wprowadź opis zdjęcia tutaj

Wiemy już z poprzednich obliczeń, gdzie celować czerwoną linią zgodnie z niebieską linią. Celując najpierw w niebieską linię:

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

Jedyne obliczenie, które się tutaj różni, to obliczenie „X Prime” (X '), ponieważ kąt między działem a wieżą (kąt „a”) zmienił odległość między liniami.

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

Ta kolejna część jest konieczna WYŁĄCZNIE, jeśli wykonujesz broń modułową (tzn. Użytkownik może zmieniać broń na wieży, a różne działa mają różne kąty). Jeśli robisz to w edytorze, możesz już zobaczyć, jaki jest kąt strzału w zależności od wieży.

Istnieją dwie metody znajdowania kąta „a”, jedna to metoda transform.up:

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

Powyższa technika będzie obliczana w 3D, więc jeśli chcesz uzyskać wynik 2D, musisz pozbyć się osi Z (to jest to, co zakładam, gdzie jest grawitacja, ale jeśli nic nie zmieniłeś, w Unity to oś Y, która jest w górę lub w dół, tj. grawitacja znajduje się na osi Y, więc może być konieczna zmiana rzeczy):

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

Drugi sposób to metoda rotacji (w tym przypadku myślę w 2D):

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

Ponownie, wszystkie te kody podadzą ci wartości, które są dodatnie, więc być może będziesz musiał dodać lub odjąć kwotę w zależności od kąta (są też obliczenia, ale nie zamierzam zagłębiać się tak dokładnie). Dobrym miejscem do rozpoczęcia tego jest Vector2.Dotmetoda w Unity.

Ostatni blok kodu dla dodatkowego wyjaśnienia tego, co robimy:

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

Jeśli zrobiłeś wszystko dobrze, powinieneś dostać taką scenę ( link do pakietu unity ): wprowadź opis zdjęcia tutaj Co rozumiem przez zawsze pozytywne wartości:wprowadź opis zdjęcia tutaj

Metoda Z może dawać wartości ujemne:wprowadź opis zdjęcia tutaj

Na przykład, pobierz pakiet unity z tego linku .

Oto kod, którego użyłem w scenie (na wieży):

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

Dostosowany kod 3D z X i Z jako płaszczyzną 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.
    }
}

Na pierwszym zdjęciu jest niewielka wada. Z to długość wieży do pudełka. X to długość wieży do pudełka po obrocie ... x = z. Dlatego, chyba że y jest przeciwprostokątną, która nie jest właściwym trójkątem, a grzech nie ma zastosowania.
The Great Duck,

@ TheGreatDuck Z nie jest odległością między wieżą a pudełkiem, to Vector2. Przed tą wieżą (jest tylko pokazany jako skończony zamiast strzałki na końcu). Nawet jeśli Z jest odległością, obraz ma jednostki i widać, że Z <X nawet nie oblicza.
John Hamilton,

2
@Franconstein, nie musisz najpierw obrócić wieży, a następnie zastosować je. Możesz je najpierw obliczyć, a następnie dodać stopień uzyskany z tych równań do stopnia obrotu wieży. (Więc zamiast obracać wieżę o 20 stopni w stosunku do obiektu, a następnie dostosowywać do pistoletu, obracałbyś wieżę o 20 stopni + regulacja dla pistoletu).
John Hamilton,

@Franconstein Zobacz nowo dostosowany kod. Ponieważ zmieniliśmy płaszczyzny, kod działał inaczej niż w innej wersji. Nie mam pojęcia, dlaczego tak się stało, ale teraz działa idealnie po mojej stronie. Zobacz: imgur.com/a/1scEH (usunięcie wież nie było konieczne, te proste modele działały tak samo jak twoje).
John Hamilton

1
@JohnHamilton Zrobiłeś to! W końcu został rozwiązany, a także laserowo precyzyjny! Dziękuję Ci! Dziękuję Ci! Dziękuję Ci! Teraz zmodyfikuję mój post tak, jak był na początku, aby można go było łatwiej zrozumieć w przyszłości! Jeszcze raz dziękuję!
Franconstein,

3

Możesz także zastosować bardziej ogólne podejście:

Matematyka Twojego problemu już istnieje w postaci produktu skalarnego (lub produktu kropkowego) . Musisz tylko uzyskać kierunki osi przedniej broni i kierunek z broni do celu.

Niech W będzie przednim wektorem twojej broni.

Niech D będzie kierunkiem od twojej broni do celu. (Target.pos - Weapon.pos)

Jeśli rozwiążesz wzór produktu kropkowego

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

dla alfa otrzymujesz:

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

Musisz tylko przekonwertować radiany na stopnie i masz kąt, aby obrócić robota. (Jak wspomniałeś, broń jest ustawiona pod kątem do robota, więc musisz dodać kąt do alfa)

wprowadź opis zdjęcia tutajwprowadź opis zdjęcia tutaj


2

Wszystkie dotychczasowe odpowiedzi są (mniej więcej) błędne, więc oto szybkie prawidłowe rozwiązanie:

wprowadź opis zdjęcia tutaj

Aby skierować broń w stronę celu, obróć wektor do przodu wieży do celu i dodaj kąt θ.

Znajdźmy więc θ:

   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.

Kiedy δ' = 0upraszcza to θ = asin(a / d), co odpowiada pierwszej części odpowiedzi Johna Hamiltona.

Edytować:

Dodałem działający przykład.

Otwórz w JSFiddle lub użyj poniższego fragmentu:

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


Dziękuję bardzo za to wyjaśnienie. Było to dla mnie wystarczająco proste i zdaje się brać pod uwagę każdą sytuację. Jednak kiedy go wdrożyłem, wyniki, które uzyskałem, były niekorzystne. Zredagowałem swój oryginalny post, aby uwzględnić mój kod, obraz wizualizujący moją konfigurację oraz wyniki dla każdej zmiennej. Wektor do przodu mojej wieży zawsze patrzy na cel, ale nawet jeśli nie, wyniki pozostają prawie takie same. czy robię coś źle? Czy to mój kod?
Franconstein,

Jeśli inne odpowiedzi „są mniej lub bardziej błędne”, nie rozumiesz / nie wdrażasz ich poprawnie. Wcześniej użyłem obu alternatywnych odpowiedzi, aby stworzyć pożądane zachowanie. @Franconstein, widzę nawet twoje komentarze na temat co najmniej jednego, który mówi, że sprawdziłeś, czy to działa. Jeśli zweryfikowałeś rozwiązanie, czy nadal masz problem?
Gnemlock,

@Gnemlock, rozwiązanie Johna Hamiltona nie było złe - wdrożyłem je i zadziałało, dlatego zweryfikowałem jego rozwiązanie jako zatwierdzone. Ale po jego wdrożeniu zacząłem wypróbowywać różne scenariusze niestatyczne, a rozwiązanie nie wytrzymało. Nie chciałem jednak przedwcześnie go wyrzucać, więc omówiłem to z kolegą. Skończyło się to potwierdzeniem, że to nie działa, ale teraz opublikowaliśmy inne możliwe rozwiązanie, a John zredagował swój post, aby go uwzględnić. W tej chwili nie mogę potwierdzić, że którekolwiek z nich działa poprawnie i nadal próbuję. Wysłałem swój kod, aby zobaczyć, czy to pomaga. Czy zrobiłem źle
Franconstein,

@Franconstein, w tej formie jest to zbyt mylące. Powiedziałbym, że ten przykład jest dobrym przykładem tego, czego można się spodziewać po czytaniu podręcznika do matematyki , ale jest zbyt mylący w odniesieniu do ogólnego programowania gier. Jedynym ważnym elementem jest kąt (który podała oryginalna odpowiedź opublikowana przez Johna Hamiltona). Rozumiem, co masz na myśli pod określonymi kątami, ostatecznie mogłeś to zrobić niepoprawnie. Uważam, że w tej odpowiedzi jest dużo miejsca, aby zrobić to niepoprawnie .
Gnemlock,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.