将方向标准化为0到360度之间

36

我正在编写一个简单的旋转例程,它将物体的旋转归一化为0到360度之间。我的 C# 代码似乎可以工作,但我并不完全满意。有人能够改进下面的代码,使其更加稳健吗?

public void Rotate(int degrees)
    {
        this.orientation += degrees;

        if (this.orientation < 0)
        {
            while (this.orientation < 0)
            {
                this.orientation += 360;
            }
        }
        else if (this.orientation >= 360)
        {
            while (this.orientation >= 360)
            {
                this.orientation -= 360;
            }
        }
    }

只是为了帮助那些偶然发现这些神奇解决方案的人。我已经测试了其中几个,用于真实的0-360值输出。使用一个能够提供+/-数千范围内值的陀螺仪,enzuguri的解决方案可以实现这一点!Saad Ahmed和QBziZ的解决方案在输入为-10时产生-10;而不是我期望的350。 希望这会有所帮助。 JC - jc508
if语句可以先去掉。 - user16217248
10个回答

61

使用模算术:

this.orientation += degrees;

this.orientation = this.orientation % 360;

if (this.orientation < 0)
{
    this.orientation += 360;
}

2
求模除是否可以得到负数的结果?这可能是浮点数问题吗? - Swanny
C语言会给你负数,我猜C#也会。由于度数是一个整数,你只能进行整数旋转。虽然方向不是明确说明是一个整数,但如果不是的话就很奇怪了。 - paxdiablo
2
哦,原来你可以这样。-1 % 360 是负1。我本以为是359。我真傻。每天都有新的学习。 - Swanny
3
(angle+3600)%360(它允许输入10个完整的负旋转) - sergio

46

这是一个可以将值归一化到任何范围的函数。

适用于在[-180,180]、[0,180]或[0,360]之间进行归一化。(不过它是用C++写的)

// Normalizes any number to an arbitrary range 
// by assuming the range wraps around when going below min or above max 
double normalize( const double value, const double start, const double end ) 
{
  const double width       = end - start   ;   // 
  const double offsetValue = value - start ;   // value relative to 0

  return ( offsetValue - ( floor( offsetValue / width ) * width ) ) + start ;
  // + start to reset back to start of original range
}

对于整数

// Normalizes any number to an arbitrary range 
// by assuming the range wraps around when going below min or above max 
int normalize( const int value, const int start, const int end ) 
{
  const int width       = end - start   ;   // 
  const int offsetValue = value - start ;   // value relative to 0

  return ( offsetValue - ( ( offsetValue / width ) * width ) ) + start ;
  // + start to reset back to start of original range
}

所以基本相同,但没有 floor 函数。 我个人使用的版本是通用的,适用于所有数字类型,并且它还使用了一个重新定义的 floor 函数,在整数类型的情况下不起作用。


3
这个函数很神奇,彻底解决了“角度归一化问题”! :) 如果您只想将一个角度归一化到[0;2 * PI [: angle -= Math.Floor(angle/(2Math.PI))2*Math.PI; - Exceptyon
该函数对非常小的数字处理能力不太好。例如输入 normalise(-1.0E-15, 0.0, 360.0),结果将是360.0。我认为结果应该比360小。 - Richy
1
你说的是对的,但非常琐碎。你不应该混淆浮点算术和实际分析数学。在最基本的层面上,浮点是:这里有一堆有限的数字,你必须尝试用最少的误差来得到分析解。稍微详细一些,浮点数是有限的分子和分母的有理数。这完全取决于你的应用领域,因为没有一个函数会对现实世界完美适用。在我的情况下,如果范围很大,这样的小数可以被视为这样的小数。 - QBziZ

22

这可以简化为以下内容。

public void Rotate (int degrees) {
    this.orientation = (this.orientation + degrees) % 360;
    if (this.orientation < 0) this.orientation += 360;
}

C#遵循与C和C++相同的规则,angle % 360将为任何整数提供介于-359359之间的值。然后第二行是为了确保它在0359的范围内(包括两个端点)。

如果您想要变得“聪明”,您可以将其缩减为一行:

this.orientation = (this.orientation + (degrees % 360) + 360) % 360;

虽然这种方法可以保证在所有情况下都是正数,但只为了节省一行代码就采用这种麻烦的方法并不值得,但我会解释一下。

degrees % 360得到的数字将在-359359之间。加上 360 将修改范围为1719。如果orientation已经为正数,则添加后仍然保证为正数,最终的% 360将其带回到0359的范围内。

至少,您可以简化您的代码,因为ifwhile 可以合并。例如,在这两行中条件的结果总是相同的,因此您不需要周围的if

if (this.orientation < 0) {
   while (this.orientation < 0) {

因此,为了实现这一点,您可以这样做:

public void Rotate (int degrees) {
    this.orientation += degrees;
    while (this.orientation <   0) this.orientation += 360;
    while (this.orientation > 359) this.orientation -= 360;
}

但我仍然会选择使用取模版本,因为它避免了潜在的昂贵循环。

当用户输入360,000,000,000作为旋转值时(相信我,他们会这样做),你的代码要处理一段时间,而此时你可能已经需要提前午餐了 :-)


1
我认为 this.orientation = (this.orientation + degrees + 360) % 360; 更易读。 - jensgram
5
@jensgram,这个问题在于,如果方向为0度,角度为-719度时,它仍然可以给出-359到-1的范围内的数字,因此您需要先对角度进行归一化,然后强制使其为正数。 - paxdiablo
@paxdiablo,你是对的!我没有预料到“度数”会超出+/-359。 - jensgram

16

我更喜欢避免使用循环,条件语句,任意偏移量 (3600),以及Math.____()函数的调用:

var degrees = -123;
degrees = (degrees % 360 + 360) % 360;
// degrees: 237

哎呀,刚看到@paxdiablo的答案提到了这个一行代码。 - Ronnie Overby
1
这对于着色器也非常有用,因为条件语句非常昂贵。 - Eli
1
为什么要使用双模运算符?"-123%360 = 237" - user3915050

11

将圆形值重新定位的公式,即保持角度在0到359之间的方法是:

angle + Math.ceil( -angle / 360 ) * 360

转向角度的通用公式如下:

angle + Math.ceil( (-angle+shift) / 360 ) * 360

其中,shift的值表示循环移位,例如如果我想要-179到180的值,则可以表示为:angle + Math.ceil( (-angle-179) / 360 ) * 360。


这是最简单的答案,适用于分数和负数。也适用于任意大的数字而不需要循环。 - Danon

10

我在AS3中快速地草拟了这个,但应该可行(在角度上可能需要使用+=

private Number clampAngle(Number angle)
{
    return (angle % 360) + (angle < 0 ? 360 : 0);
}

2

虽然有点晚,但似乎很多首选方案假设负角度不会低于-360度。这个解决方案没有这样的假设,适用于任何int可以表示的角度,并且不进行过多的乘法或除法:

public void Rotate(int degrees)
{
    this.orientation += degrees;
    
    if (this.orientation >= 0)
        this.orientation %= 360;
    else
        this.orientation = 360 - (360 - this.orientation) % 360;
}

1

我建议为规范化角度制定单独的函数-这是一种更清晰的解决方案。

public static float NormalizeEulerAngle(float angle){
    var normalized = angle % 360;
    if(normalized < 0)
        normalized += 360;
    return normalized;
}

这里有一个 Fiddle,证明了这个函数按照预期工作:https://dotnetfiddle.net/Vh4CUa

然后你可以像这样使用它:

public void Rotate(int degrees){
    orientation = NormalizeEulerAngle(orientation + degrees);
}

-1

添加任何可能输入值之间的360度的倍数(使其大于零),然后使用%获取剩余部分,就像这样

angle = 382;
normalized_angle = (angle+3600) %360;
//result = 22

上述情况可以接受最小输入角度为-3600。您可以添加任何数量(360的倍数),使输入值首先变为正数。
通常在动画过程中,您的前一帧/步骤值可能已经被前一步规范化,因此您只需添加360即可:
normalized_angle = (angle+360) %360;

-4

当将角度(以度为单位)归一化到区间[0,360>时,此函数非常方便:

float normalize_angle(float angle)
{
    float k = angle;

    while(k < 0.0)
        k += 360.0;
    while(k >= 360.0)
        k -= 360.0;
    return k;
}

不太有用。如果您在此函数中意外输入一个非常大的值,它将在循环中挂起。 - Richy
这本质上与问题中的方法相同。 - ZachB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接