¿Cómo puedo rotar un objeto basado en el desplazamiento de otro?

25

Tengo un modelo 3D de una torreta que gira alrededor del eje Y. Esta torreta tiene un cañón que está significativamente fuera del centro del objeto. Quiero que el cañón, no la torreta, apunte a un objetivo específico. Sin embargo, solo puedo rotar la torreta y, por lo tanto, no sé qué ecuación debo aplicar para lograr por objetivo.

La siguiente imagen ilustra mi problema:ingrese la descripción de la imagen aquí

Si tengo la torreta "LookAt ()" en el objetivo, un láser que se origina en el cañón perderá por completo dicho objetivo.

Si este fuera un escenario completamente de arriba hacia abajo, y el cañón fuera exactamente paralelo a la torreta, entonces mi lógica me dice que el objetivo falso debe ubicarse en una posición que sea igual al objetivo real más un desplazamiento que sea igual al Torreta y el cañón. Sin embargo, en mi escenario real, mi cámara está en ángulo 60º, y el cañón tiene una ligera rotación.

La siguiente imagen ilustra el escenario: Escenario ilustrativo

No estoy seguro exactamente por qué, pero si aplico ese mismo desplazamiento, solo parece funcionar mientras apunto a ciertas distancias de la torreta.

¿Mi lógica es defectuosa? ¿Me estoy perdiendo algo fundamental aquí?

Edición final: la solución proporcionada por la última actualización de @JohnHamilton resuelve este problema con una precisión perfecta. Ahora he eliminado el código y las imágenes que utilicé para ilustrar mis implementaciones incorrectas.

Franconstein
fuente
Desde la perspectiva del diseño de armas, podrías arreglar tu arma ;)
Wayne Werner
@WayneWerner, esta no es una opción en mi caso. Es una opción de diseño que sea torcida, pero funcional.
Franconstein
1
He agregado un ejemplo de trabajo a mi respuesta .
ens
Parece que las respuestas son perfectas ... ¿puedes mencionar qué detalle necesitas exactamente?
Seyed Morteza Kamali

Respuestas:

31

La respuesta es bastante fácil si haces los cálculos. Tiene una distancia fija de Y y una distancia variable de X (Ver Imagen 1). Necesitas encontrar el ángulo entre Z y X y girar tu torreta mucho más. ingrese la descripción de la imagen aquí

Paso 1: obtenga la distancia entre la línea de la torreta (V) y la línea de la pistola (W) que es Y (esto es constante pero no hace daño calcular). Obtenga la distancia de la torreta al objetivo (que es X).

Paso 2: divida Y entre X y luego obtenga el seno hiperbólico del valor

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.

Paso 3: gire la torreta mucho más (alrededor del eje que va de arriba hacia abajo, probablemente hacia arriba pero solo usted puede conocer esa parte).

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

Por supuesto, en este caso, es necesario que gire en sentido antihorario, por lo que es posible que deba agregar un signo menos delante del ángulo de giro allí, como en -turnAngle.

Editado algunas partes. Gracias a @ens por señalar la diferencia en la distancia.

El OP dijo que su arma tiene un ángulo, así que aquí vamos, imagen primero, explicación después: ingrese la descripción de la imagen aquí

Ya sabemos del cálculo anterior dónde apuntar la línea roja de acuerdo con la línea azul. Apuntando primero a la línea azul:

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

El único cálculo que difiere aquí es el cálculo de "X Prime" (X ') porque el ángulo entre la pistola y la torreta (ángulo "a") cambió la distancia entre las líneas.

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

La siguiente parte SÓLO es necesaria si está haciendo las pistolas de torreta modulares (es decir, el usuario puede cambiar las pistolas en una torreta y diferentes pistolas tienen ángulos diferentes). Si está haciendo esto en el editor, ya puede ver cuál es el ángulo de la pistola según la torreta.

Hay dos métodos para encontrar el ángulo "a", uno es el método transform.up:

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

La técnica anterior se calculará en 3D, por lo que si desea un resultado en 2D, debe deshacerse del eje Z (eso es lo que supongo donde está la gravedad, pero si no cambió nada, en Unity es el eje Y que está arriba o abajo, es decir, la gravedad está en el eje Y, por lo que puede que tenga que cambiar las cosas)

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 segunda forma es el método de rotación (estoy pensando en 2D en este caso):

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

Nuevamente, todos estos códigos le darán valores que son positivos, por lo que es posible que tenga que sumar o restar la cantidad dependiendo del ángulo (también hay cálculos para eso, pero no voy a profundizar en eso). Un buen lugar para comenzar con esto sería el Vector2.Dotmétodo en Unity.

Bloque final de código para una explicación adicional de lo que estamos haciendo:

//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 hiciste todo bien, deberías obtener una escena como esta ( enlace para el paquete de la unidad ): ingrese la descripción de la imagen aquí lo que quiero decir con valores siempre positivos:ingrese la descripción de la imagen aquí

El método Z puede dar valores negativos:ingrese la descripción de la imagen aquí

Para una escena de ejemplo, obtenga el paquete de la unidad desde este enlace .

Aquí está el código que he usado en la escena (en la torreta):

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

Código adaptado 3D con X y Z como plano 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
fuente
Hay un pequeño defecto en la primera imagen. Z es la longitud de la torreta hasta la caja. X es la longitud de la torreta hasta la caja después de la rotación ... x = z. Por lo tanto, a menos que y sea la hipotenusa que no es un triángulo rectángulo y sin no se aplica.
El gran pato
@TheGreatDuck Z no es la distancia entre la torreta y la caja, es el Vector2 hacia delante de esa torreta (solo se muestra finita en lugar de tener una flecha al final). Incluso si Z fuera la distancia, la imagen tiene unidades y puedes ver que Z <X sin siquiera calcular.
John Hamilton
2
@Franconstein, no tienes que girar primero la torreta, luego aplicarlas. Primero puede calcularlos, luego agregar el grado que obtiene de estas ecuaciones al grado de giro de la torreta. (Entonces, en lugar de girar la torreta 20 grados hacia el objeto, luego ajustar la pistola, giraría la torreta 20 grados + ajuste para la pistola).
John Hamilton
@Franconstein Vea el código recién ajustado. Dado que cambiamos los planos, el código estaba actuando de manera diferente que en la otra versión. No tengo idea de por qué sucedió esto, pero ahora funciona perfectamente. Consulte: imgur.com/a/1scEH (no fue necesario quitar las torretas, esos modelos simples actuaron de la misma manera que la suya).
John Hamilton
1
@JohnHamilton ¡Lo hiciste! Finalmente se resuelve, ¡y también con precisión láser! ¡Gracias! ¡Gracias! ¡Gracias! ¡Ahora editaré mi publicación como estaba al principio, para que pueda entenderse más fácilmente para referencia futura! Una vez más, gracias!
Franconstein
3

También podría usar un enfoque más general:

La matemática para su problema ya existe en forma de producto escalar (o producto de puntos) . Solo necesita obtener las direcciones del eje delantero de sus armas y la dirección de su arma al objetivo.

Deja que W sea el vector de avance de tu arma.

Deja que D sea la dirección de tu arma a tu objetivo. (Target.pos - Weapon.pos)

Si resuelve la fórmula del producto dot

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

para alfa, obtienes:

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

Solo tienes que convertir radianes a grados y tienes tu ángulo para rotar tu robot. (Como mencionaste, el arma está en ángulo con tu robot, por lo que debes agregar el ángulo a alfa)

ingrese la descripción de la imagen aquíingrese la descripción de la imagen aquí

rootmenu
fuente
2

Todas las respuestas publicadas hasta ahora son (más o menos) incorrectas, así que aquí hay una solución rápida y correcta:

ingrese la descripción de la imagen aquí

Para apuntar el arma hacia el objetivo, gire el vector de la torreta hacia el objetivo y agregue el ángulo θ.

Entonces encontremos θ:

   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.

Cuando δ' = 0esto se simplifica aθ = asin(a / d) , que coincide con la primera parte de la respuesta de John Hamilton.

Editar:

He agregado un ejemplo de trabajo.

Abra en JSFiddle o use el fragmento incrustado a continuación:

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
fuente
Muchas gracias por esta explicación. Para mí fue bastante simple de entender, y parece tener en cuenta cada situación. Sin embargo, cuando lo implementé, los resultados que obtuve no fueron favorables. He editado mi publicación original para incluir mi código, una imagen que visualice mi configuración y los resultados para cada variable. El vector de avance de mi torreta siempre está mirando al objetivo, pero incluso si no es así, los resultados siguen siendo casi los mismos. ¿Estoy haciendo algo mal? ¿Es mi código?
Franconstein
Si las otras respuestas "están más o menos equivocadas", no las comprende / implementa correctamente. He usado ambas respuestas alternativas, previamente, para crear el comportamiento deseado. @Franconstein, incluso veo tus comentarios en al menos uno para decir que has verificado que funciona. Si ha verificado una solución, ¿todavía tiene un problema?
Gnemlock
@Gnemlock, la solución de John Hamilton no estuvo mal: la implementé y funcionó, y así verifiqué su solución como aprobada. Pero después de implementarlo, comencé a probar diferentes escenarios no estáticos, y la solución no se mantuvo. Sin embargo, no quería descartarlo prematuramente, así que lo revisé con un colega. Terminamos confirmando que no se cumple, pero ahora hemos publicado otra posible solución, y John editó su publicación para incluirla. A partir de este momento, no puedo confirmar que ninguno de ellos funcione correctamente, y todavía estoy intentando. Publiqué mi código para ver si ayuda. ¿Hice mal?
Franconstein
@Franconstein, en esta forma es demasiado confuso. Diría que este ejemplo es un buen ejemplo de lo que cabría esperar al leer un libro de texto de matemáticas , pero es extremadamente confuso en relación con la programación general de juegos. El único elemento importante es el ángulo (que proporcionó la respuesta original que John Hamilton publicó). Veo lo que quiere decir con ángulos particulares, en última instancia, puede haber hecho esto incorrectamente. Encuentro que hay mucho espacio, en esta respuesta, para hacerlo incorrectamente .
Gnemlock