Como rotacionar um objeto com base no deslocamento de outro para ele?

25

Eu tenho um modelo 3D de uma torre que gira em torno do eixo Y. Esta torre possui um canhão que fica significativamente fora do centro do objeto. Quero que o canhão, não a torre, mire em um alvo especificado. Porém, só posso girar a torre e, portanto, não sei que equação preciso aplicar para realizar por objetivo.

A imagem a seguir ilustra meu problema:insira a descrição da imagem aqui

Se eu tiver a torre "LookAt ()" no alvo, um laser originário do canhão errará completamente o alvo.

Se esse era um cenário completamente descendente e o canhão estivesse exatamente paralelo à torre, minha lógica me diz que o alvo falso deve estar localizado em uma posição igual ao alvo real mais um deslocamento igual ao entre o torre e canhão. No entanto, no meu cenário atual, minha câmera está angulada a 60º e o canhão tem uma leve rotação.

A imagem a seguir ilustra o cenário: Cenário Ilustrativo

Não sei exatamente por que, mas se eu aplicar esse mesmo deslocamento, ele apenas funcionará enquanto visar certas distâncias da torre.

Minha lógica é falha? Estou perdendo algo fundamental aqui?

Edição final: a solução fornecida pela atualização mais recente do @JohnHamilton resolve esse problema com precisão perfeita. Agora removi o código e as imagens que usei para ilustrar minhas implementações incorretas.

Franconstein
fonte
Do ponto de vista do design de armas, você pode consertar sua arma ;) #
Wayne Werner
@WayneWerner isso não é uma opção no meu caso. É uma opção de design que seja torta, mas funcional.
21716 Franconstein
11
Eu adicionei um exemplo de trabalho à minha resposta .
ens
Parece que as respostas são perfeitas ... você pode mencionar exatamente quais detalhes você precisa?
Seyed Morteza Kamali

Respostas:

31

A resposta é realmente muito fácil se você fizer as contas. Você tem uma distância fixa de Y e uma distância variável de X (veja a figura 1). Você precisa descobrir o ângulo entre Z e X e transformar sua torre muito mais. insira a descrição da imagem aqui

Etapa 1 - Obtenha a distância entre a linha da torre (V) e a linha da arma (W), que é Y (isso é constante, mas não custa calcular). Distância da torre ao alvo (que é X).

Etapa 2 - Divida Y por X e obtenha o Seno Hiperbólico do 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.

Etapa 3 - Gire a torre muito mais (em torno do eixo que vai de cima para baixo, provavelmente acima do eixo, mas somente você pode conhecer essa parte).

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

É claro que, nesse caso, você precisa girar no sentido anti-horário para adicionar um sinal de menos na frente do turnAngle, como em -turnAngle.

Editou algumas partes. Agradecemos a @ens por apontar a diferença de distância.

O OP disse que sua arma tinha um ângulo, então aqui vamos nós, primeiro a imagem, a explicação depois: insira a descrição da imagem aqui

Já sabemos no cálculo anterior onde apontar a linha vermelha de acordo com a linha azul. Então, visando a linha azul primeiro:

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

O único cálculo que difere aqui é o cálculo de "X Prime" (X ') porque o ângulo entre a pistola e a torre (ângulo "a") alterou a distância entre as linhas.

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

Esta próxima parte é SOMENTE necessária se você estiver usando as armas de torre modulares (ou seja, o usuário pode mudar as armas em uma torre e armas diferentes têm ângulos diferentes). Se você estiver fazendo isso no editor, já poderá ver qual é o ângulo da pistola de acordo com a torre.

Existem dois métodos para encontrar o ângulo "a", um é o método transform.up:

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

A técnica acima será calculada em 3D. Portanto, se você deseja um resultado 2D, precisa se livrar do eixo Z (é isso que eu suponho onde está a gravidade, mas se você não mudou nada, no Unity é o eixo Y que está para cima ou para baixo, ou seja, a gravidade está no eixo Y, então você pode precisar mudar as coisas):

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

A segunda maneira é o método de rotação (estou pensando em 2D neste caso):

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

Novamente, todos esses códigos fornecerão valores positivos, portanto, você pode adicionar ou subtrair a quantidade dependendo do ângulo (também existem cálculos para isso, mas não vou detalhar isso). Um bom lugar para começar com isso seria o Vector2.Dotmétodo no Unity.

Bloco final de código para explicação adicional do que estamos fazendo:

//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 você fez tudo certo, deve ter uma cena como esta ( link para o unitypackage ): insira a descrição da imagem aqui O que quero dizer com valores sempre positivos:insira a descrição da imagem aqui

O método Z pode dar valores negativos:insira a descrição da imagem aqui

Para uma cena de exemplo, obtenha o unitypackage neste link .

Aqui está o código que usei na cena (na torre):

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 3D adaptado com X e 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
fonte
Há uma pequena falha na primeira imagem. Z é o comprimento da torre da caixa. X é o comprimento da torre da caixa após a rotação ... x = z. Portanto, a menos que y seja a hipotenusa que não é um triângulo retângulo e o pecado não se aplica.
The Great Duck
O @TheGreatDuck Z não é a distância entre a torre e a caixa; é o Vector2.forward dessa torre (é apenas mostrado finito em vez de ter uma seta no final). Mesmo que Z estivesse à distância, a imagem tem unidades e você pode ver que Z <X sem calcular.
John Hamilton
2
@ Frankconstein, você não precisa primeiro girar a torre e aplicá-los. Você pode primeiro calculá-las e depois adicionar o grau que obtém dessas equações ao grau de giro da torre. (Então, em vez de girar a torre 20 graus em relação ao objeto e, em seguida, ajustar a arma, você giraria a torre 20 graus + ajuste para a arma).
John Hamilton
@Franconstein Veja o código recém-ajustado. Desde que mudamos de avião, o código estava agindo de maneira diferente do que na outra versão. Não tenho idéia do por que isso aconteceu, mas agora está funcionando perfeitamente do meu lado. Veja: imgur.com/a/1scEH (não foi necessário remover suas torres, esses modelos simples agiram da mesma maneira que o seu).
John Hamilton
11
@JohnHamilton Você conseguiu! É finalmente resolvido, e com precisão semelhante a laser também! Obrigado! Obrigado! Obrigado! Agora vou editar meu post como ele estava no começo, para que ele possa ser mais facilmente entendido para referência futura! Mais uma vez obrigado!
21716 Franconstein
3

Você também pode usar uma abordagem mais geral:

A matemática do seu problema já existe na forma do produto escalar (ou produto escalar) . Você só precisa obter as direções do seu eixo dianteiro e a direção da sua arma para o alvo.

Seja W o vetor avançado da sua arma.

Seja D a direção da sua arma para o seu alvo. (Target.pos - Arma.pos)

Se você resolver a fórmula do produto escalar

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

para alfa, você obtém:

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

Você só precisa converter radianos em graus e tem seu ângulo para girar seu robô. (Como você mencionou, a arma está em ângulo com o seu robô, então você precisa adicionar o ângulo em alfa)

insira a descrição da imagem aquiinsira a descrição da imagem aqui

rootmenu
fonte
2

Todas as respostas postadas até agora estão (mais ou menos) erradas, então aqui está uma solução rápida e correta:

insira a descrição da imagem aqui

Para apontar a arma na direção do alvo, gire o vetor da torre para frente e adicione o ângulo θ.

Então, vamos encontrar θ:

   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 δ' = 0isso simplifica para θ = asin(a / d), o que corresponde à primeira parte da resposta de John Hamilton.

Editar:

Eu adicionei um exemplo de trabalho.

Abra no JSFiddle ou use o snippet incorporado abaixo:

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
fonte
Muito obrigado por esta explicação. Foi simples o suficiente para eu entender, e parece levar em consideração todas as situações. No entanto, quando o implementei, os resultados obtidos não foram favoráveis. Editei minha postagem original para incluir meu código, uma imagem visualizando minha configuração e os resultados de cada variável. O vetor de avanço da minha torre está sempre olhando para o alvo, mas mesmo se não for, os resultados permanecem quase os mesmos. Estou fazendo algo errado? É o meu código?
Franconstein
Se as outras respostas "estão mais ou menos erradas", você não as entende / implementa corretamente. Eu usei as duas respostas alternativas, anteriormente, para criar o comportamento desejado. @ Frankconstein, eu até vejo seus comentários sobre pelo menos um para dizer que você verificou que funciona. Se você verificou uma solução, ainda tem algum problema?
Gnemlock
@Gnemlock, a solução de John Hamilton não estava errada - eu a implementei e funcionou e, portanto, verifiquei sua solução como aprovada. Mas, após implementá-lo, comecei a tentar diferentes cenários não estáticos, e a solução não se sustentou. Porém, eu não queria descartá-lo prematuramente, por isso examinei-o com um colega. Acabamos confirmando que não é válido, mas agora publicamos outra solução possível, e John editou sua postagem para incluí-la. A partir deste momento, não posso confirmar que nenhum deles funcione corretamente e ainda estou tentando. Postei meu código para ver se ajuda. Eu fiz errado?
Franconstein
@Franconstein, nesta forma, é excessivamente confuso. Eu diria que este exemplo é um bom exemplo do que você esperaria ler um livro de matemática , mas é excessivamente confuso em relação à programação geral de jogos. O único elemento importante é o ângulo (que a resposta original que John Hamilton postou forneceu). Entendo o que você quer dizer com ângulos particulares, em última análise, você pode ter feito isso incorretamente. Acho que há muito espaço, nesta resposta, para fazê-lo incorretamente .
Gnemlock