如何根据另一个对象的偏移旋转对象?


25

我有一个围绕Y轴旋转的转塔的3D模型。该炮塔的加农炮明显偏离物体中心。我希望大炮而不是炮塔瞄准特定目标。但是,我只能旋转炮塔,因此我不知道为了达到目标而需要应用什么方程式。

下图说明了我的问题:在此处输入图片说明

如果我的炮塔为“ LookAt()”目标,则来自加农炮的激光将完全错过所述目标。

如果这是完全自上而下的情况,并且加农炮与炮塔完全平行,那么我的逻辑告诉我,假目标应位于等于实际目标的位置,再加上与目标之间的偏移量相等的位置。炮塔和加农炮 但是,在我的实际情况下,我的摄像机倾斜60º,并且大炮有轻微的旋转。

下图说明了这种情况: 说明性场景

我不确定为什么,但是如果应用相同的偏移量,则它似乎仅在瞄准与转塔一定距离的同时起作用。

我的逻辑有缺陷吗?我在这里缺少基本的东西吗?

最终编辑:@JohnHamilton最新更新提供的解决方案以完美的精度解决了此问题。现在,我已经删除了用于说明错误实现的代码和图像。


从武器设计的角度来看,您只需要修理枪支即可;)
韦恩·沃纳

@WayneWerner这不是我的选择。使其弯曲但功能正常是一种设计选择。
Franconstein '16

1
我在答案中添加了一个有效的示例。
ens

看来答案是完美的...您能确切地说出您需要什么细节吗?
Seyed Morteza Kamali

Answers:


31

如果进行数学运算,答案实际上很简单。您有一个固定的Y距离和一个可变的X距离(见图1)。您需要找出Z和X之间的角度,并进一步旋转炮塔。 在此处输入图片说明

第1步-获得炮塔线(V)和炮线(W)之间的距离,该距离为Y(这是常数,但计算时不会受到伤害)。获取转塔到目标的距离(X)。

步骤2-将Y除以X,然后获得该值的双曲正弦值

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.

第3步-进一步旋转刀塔(围绕从顶部到底部的轴,最有可能是向上轴,但只有您知道那部分)。

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

当然,在这种情况下,您需要将其逆时针旋转,因此您可能需要在turnAngle的前面添加一个负号,如中所示-turnAngle

编辑了一些部分。感谢@ens指出距离的差异。

OP说他的枪有一个角度,所以我们开始,首先是图像,然后是解释: 在此处输入图片说明

从前面的计算中我们已经知道将红线对准蓝线的位置。因此,首先瞄准蓝线:

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

此处唯一不同的计算是“ X Prime”(X')的计算,因为喷枪和炮塔之间的角度(角度“ a”)改变了线之间的距离。

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

如果您要对炮塔模块进行模块化,则只有下一部分是必需的(即,用户可以在炮塔上更换喷枪,并且不同的喷枪具有不同的角度)。如果在编辑器中执行此操作,您已经可以看到根据炮塔角度的枪支角度。

有两种找到角度“ a”的方法,一种是transform.up方法:

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

上面的技术将以3D进行计算,因此,如果要获得2D结果,则需要摆脱Z轴(这是我假设重力所在的位置,但是如果您不进行任何更改,则在Unity中,Y轴是向上还是向下,也就是说,重力在Y轴上,因此您可能必须进行更改):

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

第二种方法是旋转方法(在这种情况下,我正在考虑2D):

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

同样,所有这些代码都将为您提供正值,因此您可能必须根据角度来增加或减少该数量(也有相应的计算方法,但我不会对此进行深入介绍)。一个很好的起点是Vector2.DotUnity中的方法。

最后的代码块,用于进一步说明我们在做什么:

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

如果您做对了所有事情,那么应该会得到这样的场景(unitypackage的链接): 在此处输入图片说明 我的意思是始终为正值:在此处输入图片说明

Z方法可以给出负值:在此处输入图片说明

对于示例场景,请从此链接获取unitypackage

这是我在场景中(转塔上)使用的代码:

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

X和Z为2D平面的3D适应代码:

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

第一张图片中有轻微瑕疵。Z是转塔到盒子的长度。X是旋转后转塔到盒子的长度... x = z。因此,除非y是不是直角三角形的斜边,否则sin不适用。
伟大的鸭子

@TheGreatDuck Z不是炮塔与盒子之间的距离,它是炮塔的Vector2.forward(它只是有限显示的,而不是末端带有箭头)。即使Z是距离,图片也具有单位,即使不计算也可以看到Z <X。
约翰·汉密尔顿

2
@Franconstein,您不必首先转动炮塔,然后应用这些。您可以首先计算这些值,然后将这些方程式获得的度数添加到转塔的转弯度数中。(因此,您无需将炮塔与物体旋转20度,而是针对喷枪进行调整,而是将炮塔旋转20度+对喷枪进行调整)。
约翰·汉密尔顿

@Franconstein请参阅新调整的代码。由于我们更换了飞机,因此代码的行为不同于其他版本。我不知道为什么会这样,但是现在我可以正常运行了。请参阅:imgur.com/a/1scEH无需拆卸炮塔,那些简单的模型的行为与您的相同)。
约翰·汉密尔顿

1
@JohnHamilton,您做到了!终于解决了,并且具有类似激光的精度!谢谢!谢谢!谢谢!现在,我将编辑帖子的内容,使其与开始时的样子一样,以便您更容易理解,以备将来参考!再次感谢您!
Franconstein '16

3

您还可以使用更通用的方法:

您的问题的数学运算已经以标量积(或点积)的形式存在。您只需要获取武器前进轴的方向以及从武器到目标的方向即可。

令W为武器的前进向量。

令D为从武器到目标的方向。(Target.pos-Weapon.pos)

如果您解决点积的公式

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

对于alpha,您将获得:

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

您只需要将弧度转换为度,就可以旋转机器人了。(正如您提到的,武器与机器人成一定角度,因此您需要将角度添加到alpha中)

在此处输入图片说明在此处输入图片说明


2

到目前为止发布的所有答案都是(或多或少)错误的,因此,这里有一个快速正确的解决方案:

在此处输入图片说明

为了将枪对准目标,请将炮塔正向矢量旋转至目标并添加角度θ。

因此,让我们找到θ:

   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.

如果δ' = 0简化为θ = asin(a / d),则与约翰·汉密尔顿答案的第一部分匹配。

编辑:

我添加了一个有效的示例。

在JSFiddle中打开或使用下面的嵌入式代码段:

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


非常感谢您的解释。对于我来说,这很简单,而且似乎已考虑到每种情况。但是,当我实施它时,获得的结果并不令人满意。我已经编辑了原始帖子,以包含我的代码,可视化我的设置的图像以及每个变量的结果。我的炮塔的前向矢量始终盯着目标,但是即使不是,结果仍然几乎相同。难道我做错了什么?是我的代码吗?
Franconstein

如果其他答案“或多或少是错误的”,则说明您未正确理解/正确实施它们。以前,我已经使用了两个替代答案来创建所需的行为。@Franconstein,我什至看到您对至少一个的评论,说您已经验证了它的有效性。如果您已经验证了解决方案,是否还有问题?
Gnemlock '16

@Gnemlock,约翰·汉密尔顿的解决方案没有错-我实现了它,并且它起作用了,因此我验证了他的解决方案是否得到批准。但是在实施它之后,我开始尝试不同的非静态方案,但是该解决方案没有成功。但是,我不想过早丢弃它,所以我和一位同事一起进行了研究。我们最终确认它不成立,但是现在ens发布了另一个可能的解决方案,John编辑了他的帖子以包括它。目前,我无法确定它们中的任何一个都可以正常工作,并且仍在尝试中。我发布了代码以查看是否有帮助。我做错了吗?
Franconstein

@Franconstein,这种形式过于混乱。我想说这个例子是阅读数学课本的一个很好的例子,但是与一般的游戏编程相比,这简直令人困惑。唯一重要的元素是角度(John Hamilton发布的原始答案确实提供了角度)。我明白您所说的特定角度是什么意思,最终您可能做错了。我发现这个答案有很大的空间来做不正确的事情
Gnemlock '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.