Why should I need to convert an angle as radians

Hi everyone,

When I started to mess around with the arduboy, I did not think at the time it would be so challenging to go back to mathematics. Which is a great thing, I guess, because I have a lot to learn.

So, I have a triangle represented by 3 lines between 3 points. I know there is a method drawTriangle(), but I just want to make it more… “raw”.

When I press the left and right arrow, the triangle should rotates from its center. I found the formula to make it rotate from the origin (0,0). So I understood I need to translate it, make it rotate, the translate back. But for what the topic is about, let’s it make rotate around the origin.

I have an angle attribute on my triangle object, which give me the actual angle. I want this angle to be in degrees (0 to 360);

Every time you press left or right, I add / subtract 1 to the angle attribute of my triangle object.

I have this formulas to rotate one point of the triangle :

/**
 * Rotates a point
 */
double rotatedXPoint(uint8_t X, uint8_t Y)
{
    return X* cos(angle) - Y * sin(angle);
}

/**
 * Rotates a point
 */
double rotatedYPoint(uint8_t X, uint8_t Y)
{
    return Y * cos(angle) + X * sin(angle);
} 

So what I thought how it would react is :

  • if my angle is 90, the triangle should be here :

90degrees

  • if my angle is 270, it should be a the opposite.

What bothers me, it’s that I have to add radians(angle) to my formulas to make it work as I think, and I don’t understand why…

Could someone bring me an explanation please ?

2 Likes

The trigonometry function is from the Arduino base library and simply requires input in radians:
https://www.arduino.cc/reference/en/language/functions/trigonometry/cos/

May be worth Googling for more guidance, many people have also asked this.
Good luck :slight_smile:

1 Like

Damned, I was so in “mathematic learning mode” that I didn’t even think about that. I assumed the issue was in my understandings of mathematics…

Thank @acedent, it made my day.

2 Likes

Maybe you want to land here:

1 Like

The short answer is that sin and cos, as they are typically implemented in programming languages and libraries, use radians because ‘proper’ mathmaticians use radians.

Tthe Babylonian system of degrees is only taught to students to make their lives a bit easier and isn’t really used in ‘proper’ mathematics.

Actually, it’s from avr-libc, the implementation of the standard C library that underpins Arduino:
https://www.nongnu.org/avr-libc/user-manual/group__avr__math.html


The long answer…

Theoretically it’s entirely possible to write versions of sin and cos that accept degrees (or indeed other units) instead of radians, but typically sin and cos implementations found in programming languages and libraries tend to use radians instead.

Radians actually make more sense when you know how they’re defined.
Radians are the ratio between the arc length and the radius of the circle (i.e. radians = arc length / radius).
Hence 1 radian is the angle at which the arc length and the radius are equal, which for the unit circle (a circle with a radius of 1) means the arc length is also 1.
This has the useful side effect of a half turn being π radians, and a full turn being 2π radians.
(Though personally I’m a tauist.)

Degrees on the other hand only exist because Babylonians used sexagesimal (a base 60 numeral system) and hence decided to divide their circles into 6 60-degree chunks.

Really the units you use to divide a circle are completely arbitrary though.
You could develop a unit that divides a circle into 100 pieces or 1000 (and had we not inherited the Babylonian system, that might well be what we’d be using today).

Another system is brads (‘binary radians’), which divide a circle into 256 (or 65536) divisions for the sake of making angles easier to calculate on binary computers.

Personally I like to use ‘turns’, where 1.0 is a full circle, 0.5 is a semicircle and converting to any other system is easy - multiply by 360 to get degrees, multiply by 256 to get brads, multiply by 2π (or τ as I prefer) to get radians.

But ultimately, the standard sin and cos provided by the majority of programming languages tend to be geared towards radians because that’s what the ‘hardcore’ mathematicians use.

In fact computers that have an FPU will often have hardware sin and cos implementations that use radians, and the sin and cos functions provided by languages or their libraries tend to make use of those. The Arduboy doesn’t have an FPU, but pretty much every desktop, laptop and tablet does.

Implementations that accept degrees are exceedingly uncommon.
I think I’ve seen a variant of BASIC that accepted degrees once, or something along those lines, definitely something targetted at beginners or students, but never in a serious programming language.


Yes, specifically you:

  • Translate it so that the centre is at point (0,0) (the origin)
  • Then rotate, which will cause the points of the triangle to rotate about the origin
  • Translate it back to its previous position

If you’re going to do a lot of those kinds of operations,
you should probably read more about matrices.

Here’s a few resources to get you started:

Though honestly, this kind of heavy mathematics is likely to chew up the Arduboy’s resources very quickly, so you’ll either have to use it sparingly or spend a lot of effort optimising it.
(E.g. using fixed points instead of floating points, using brads instead of radians.)

4 Likes

Thanks @Pharap for that full and beyond answer. Yeah, I find the youtube channel of Jorge Rodriguez very useful, you made me discovering it from another post answer !

So yeah, now i have to learn matrices.

I’m actually very satisfied that I’ve chosen the Arduboy to start making games, because I think it’s a good start to understand how game physics work. I think that I would not go that deep if I’d use an existing game engine.

See you

4 Likes

I’m surprised you didn’t mention the gradian here, which divides a circle into 400 units or 100 per quarter circle (100 per 90 degrees). It’s available (abbreviated to grad), along with degrees and radians, on every calculator I’ve seen that has trig functions.

In all honesty I forgot they existed. I’ve never really used them.
The last and only time I’ve encountered them was when I read my calculator’s manual,
which was quite some time ago.

1 Like

This :arrow_up: :100:

IMHO it’s the best system for internal angles.

It helps in reducing rounding errors and drift, at least making it much more predictable.

fract(x) function or (x - floor(x)) to stay within a unit circle, or better: using fixed points and overflow/underflow as the wrap-around is free (eg uint32_t * 0x1.0p-32 ).

( To be pedant it’s technically an “undefined behaviour” but show me a modern computer system that isn’t two’s-complement… And if there’s money in it I’ll just write a custom clang++ target to make it work in about a week :grin: )

Bonus: casting between the signed/unsigned versions correctly changes the range between [0 … 1) and [-0.5 … 0.5) to get a signed differences in heading.

Without the variability of floating point rounding errors and is consistent cross-platform.

Want to know which quadrant you’re heading to? Just bit-mask it.
Want to snap angles to 1/8th circle (45 degrees) ? add 1/16 and bit-mask it.

Can even use hexadecimal float constants with a modern compiler to enter nice exact binary numbers.

And you can directly shove them into shader values for doing texture lookups on the GPU (eg: for occlusion shading) which need to be in a unit range, or table lookups on CPU.

It’s so beautiful, it’s almost magical.

2 Likes

As a bonus: technically brads are equivalent to the fractional part of a fixed point in an unsigned QN.8 format.

Since fixed points were mentioned, here’s something from my archive:

It could do with a tidy-up (not least because I forgot the inlines), but technically it’ll work on Arduboy.

Only for signed types.

For unsigned types the behaviour is well defined.
By the rules of the standard an unsigned type implements arithmetic modulo a power of two.

Where you have to be careful though is that because of the rule that signed overflow is undefined behaviour, the compiler is allowed to legally optimise away certain operations that you might not want it to.
For example, given the following:

if (a + 100 < a)
  throw std::exception();

An optimising compiler is legally allowed to assume that (a + 100 < a) will always evaluate to false and thus completely eliminate both the comparison and the body of the if statement.

That may seem strange, but apparently both Clang and GCC have done so historically,
and they may well still do this (I haven’t checked).

1 Like

yes, same for

if (!this) { 
    cerr << "big problem" 
     << "...here's some diagnosis to save you literally days of debugging time..." ;
    abort(); 
}

which the compiler “helpfully” eliminates and the GCC language-lawyers refuse(d) to NOT-optimise even at -O0 because the standard says this cannot be null.

It’s kind of like saying by law cars aren’t legally allowed to be stolen so we’ve removed all locks and if you explicitly put one the dealer’s mechanic shop will remove it because “you don’t legally need it”.

I was not happy.

In fairness, if this is null then you have some pretty serious issues.
(For one, it implies that you probably called the function by using -> on a null pointer, which is equivalent to (*object).function(), so you’re already in the realm of undefined behaviour long before if(!this) is reached.)

Though I think this is a good argument for a compiler flag that does absolutely no optimisation whatsoever and always does everything as you wrote it, no matter how inefficient that may be.

I create the same library but using both double or Integers for faster execution and SIN/COS tables, is done in C# I was to lazy to do it on a uC:.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{

    struct _2d_point_i
    {
        public Int16 x;
        public Int16 y;
        public Int16 depth;
        public Int16 scaleFactor;
    };

    struct _2d_point
    {
        public double x;
        public double y;
        public double depth;
        public double scaleFactor;
    };

    struct _3d_points_i
    {
        public Int32[] x;
        public Int32[] y;
        public Int32[] z;
        public Int32[] depth;
        public Int32[] scaleFactor;
        public Int32 focalLength;
        public Int32 depthScale;
        public UInt16 length;
    };

    struct _3d_points
    {
        public double[] x;
        public double[] y;
        public double[] z;
        public double[] depth;
        public double[] scaleFactor;
        public double focalLength;
        public double depthScale;
        public UInt16 length;
    };

    struct axisRotations_i
    {
        public Int32 x;
        public Int32 y;
        public Int32 z;
    };

    struct axisRotations
    {
        public double x;
        public double y;
        public double z;
    };

    Int16[] sin = new Int16[91];
    Int16[] cos = new Int16[91];

    static Bitmap MyImage = new Bitmap(512, 512);
    static Graphics graphic = Graphics.FromImage(MyImage);
    _3d_points Points;
    _3d_points_i Points_i;

    public Form1()
    {
        InitializeComponent();
    }

    Int16 get_sin(UInt16 deg)
    {
        UInt16 degIn = (UInt16)(deg % 359);
        if (degIn <= 90)
            return sin[degIn];
        else if (degIn <= 180)
            return sin[180 - degIn];
        else if (degIn <= 270)
            return (Int16)(-sin[degIn - 180]);
        else
            return (Int16)(-sin[360 - degIn]);
    }

    Int16 get_cos(UInt16 deg)
    {
        UInt16 degIn = (UInt16)(deg % 359);
        if (degIn <= 90)
            return cos[degIn];
        else if (degIn <= 180)
            return (Int16)(-cos[180 - degIn]);
        else if (degIn <= 270)
            return (Int16)(-cos[degIn - 180]);
        else
            return cos[360 - degIn];
    }

    void make2DPointI(_2d_point_i[] _point, UInt32 Cell, Int16 x, Int16 y, Int16 depth, Int16 scaleFactor)
    {
        _point[Cell].x = x;
        _point[Cell].y = y;
        _point[Cell].depth = depth;
        _point[Cell].scaleFactor = scaleFactor;
    }

    void make2DPoint(_2d_point[] _point, UInt32 Cell, double x, double y, double depth, double scaleFactor)
    {
        _point[Cell].x = x;
        _point[Cell].y = y;
        _point[Cell].depth = depth;
        _point[Cell].scaleFactor = scaleFactor;
    }

    void Transform3Dto2D_i(_2d_point[] screenPoints, _3d_points_i Points, axisRotations_i AxisRotations)
    {
        Int16 sx = get_sin((UInt16)(AxisRotations.x));
        Int16 cx = get_cos((UInt16)(AxisRotations.x));
        Int16 sy = get_sin((UInt16)(AxisRotations.y));
        Int16 cy = get_cos((UInt16)(AxisRotations.y));
        Int16 sz = get_sin((UInt16)(AxisRotations.z));
        Int16 cz = get_cos((UInt16)(AxisRotations.z));
        Int32 x, y, z, xy, xz, yx, yz, zx, zy, scaleFactor;

        UInt32 i = Points.length;
        while (i-- != 0)
        {
            x = Points.x[i];
            y = Points.y[i];
            z = Points.z[i];

            // rotation around x
            xz = (Int32)((Int32)(sx * y) + (Int32)(cx * z)) / 65536;
            xy = (Int32)((Int32)(cx * y) - (Int32)(sx * z)) / 65536;
            // rotation around y
            yx = (Int32)((Int32)(sy * xz) + (Int32)(cy * x)) / 65536;
            yz = (Int32)((Int32)(cy * xz) - (Int32)(sy * x)) / 65536;
            // rotation around z
            zy = (Int32)((Int32)(sz * yx) + (Int32)(cz * xy)) / 65536;
            zx = (Int32)((Int32)(cz * yx) - (Int32)(sz * xy)) / 65536;

            scaleFactor = (Int32)((Points.focalLength * 65536) / ((Points.focalLength + yz)));
            x = (Int32)((Int32)(zx * scaleFactor) / (Points.depthScale * 65536));
            y = (Int32)((Int32)(zy * scaleFactor) / (Points.depthScale * 65536));
            z = (Int32)((yz * 65536) / (Points.depthScale * 65536));

            make2DPoint(screenPoints, i, x, y, -z, scaleFactor);
        }
    }

    void Transform3Dto2D(_2d_point[] screenPoints, _3d_points Points, axisRotations AxisRotations)
    {
        double sx = Math.Sin(AxisRotations.x);
        double cx = Math.Cos(AxisRotations.x);
        double sy = Math.Sin(AxisRotations.y);
        double cy = Math.Cos(AxisRotations.y);
        double sz = Math.Sin(AxisRotations.z);
        double cz = Math.Cos(AxisRotations.z);
        double x, y, z, xy, xz, yx, yz, zx, zy, scaleFactor;

        UInt32 i = Points.length;
        while (i-- != 0)
        {
            x = Points.x[i];
            y = Points.y[i];
            z = Points.z[i];

            // rotation around x
            xy = cx * y - sx * z;
            xz = sx * y + cx * z;
            // rotation around y
            yz = cy * xz - sy * x;
            yx = sy * xz + cy * x;
            // rotation around z
            zx = cz * yx - sz * xy;
            zy = sz * yx + cz * xy;

            scaleFactor = Points.focalLength / (Points.focalLength + yz);
            x = (zx * scaleFactor) / Points.depthScale;
            y = (zy * scaleFactor) / Points.depthScale;
            z = yz / Points.depthScale;

            make2DPoint(screenPoints, i, x, y, -z, scaleFactor);
        }
    }

    private void paint_3d_triangle_i(_3d_points_i Points, Int16 X_offset, Int16 Y_offset, Int16 X_Angle, Int16 Y_Angle, Int16 Z_Angle)
    {
        _2d_point[] screenPoints = new _2d_point[3];

        axisRotations_i cubeAxisRotations;
        cubeAxisRotations.y = Y_Angle;
        cubeAxisRotations.x = X_Angle;
        cubeAxisRotations.z = Z_Angle;
        Transform3Dto2D_i(screenPoints, Points, cubeAxisRotations);

        Int32 X_start = (Int32)screenPoints[0].x;
        Int32 Y_start = (Int32)screenPoints[0].y;
        Int32 X_end = (Int32)screenPoints[1].x;
        Int32 Y_end = (Int32)screenPoints[1].y;
        graphic.DrawLine(Pens.Red, X_offset + X_start, Y_offset + Y_start, X_offset + X_end, Y_offset + Y_end);

        X_start = (Int32)screenPoints[1].x;
        Y_start = (Int32)screenPoints[1].y;
        X_end = (Int32)screenPoints[2].x;
        Y_end = (Int32)screenPoints[2].y;
        graphic.DrawLine(Pens.Red, X_offset + X_start, Y_offset + Y_start, X_offset + X_end, Y_offset + Y_end);

        X_start = (Int32)screenPoints[2].x;
        Y_start = (Int32)screenPoints[2].y;
        X_end = (Int32)screenPoints[0].x;
        Y_end = (Int32)screenPoints[0].y;
        graphic.DrawLine(Pens.Red, X_offset + X_start, Y_offset + Y_start, X_offset + X_end, Y_offset + Y_end);
    }

    private void paint_3d_triangle(_3d_points Points, Int32 X_offset, Int32 Y_offset, double X_Angle, double Y_Angle, double Z_Angle)
    {
        _2d_point[] screenPoints = new _2d_point[3];

        axisRotations cubeAxisRotations;
        cubeAxisRotations.y = Y_Angle;
        cubeAxisRotations.x = X_Angle;
        cubeAxisRotations.z = Z_Angle;
        Transform3Dto2D(screenPoints, Points, cubeAxisRotations);

        Int32 X_start = (Int32)screenPoints[0].x;
        Int32 Y_start = (Int32)screenPoints[0].y;
        Int32 X_end = (Int32)screenPoints[1].x;
        Int32 Y_end = (Int32)screenPoints[1].y;
        graphic.DrawLine(Pens.Red, X_offset + X_start, Y_offset + Y_start, X_offset + X_end, Y_offset + Y_end);

        X_start = (Int32)screenPoints[1].x;
        Y_start = (Int32)screenPoints[1].y;
        X_end = (Int32)screenPoints[2].x;
        Y_end = (Int32)screenPoints[2].y;
        graphic.DrawLine(Pens.Red, X_offset + X_start, Y_offset + Y_start, X_offset + X_end, Y_offset + Y_end);

        X_start = (Int32)screenPoints[2].x;
        Y_start = (Int32)screenPoints[2].y;
        X_end = (Int32)screenPoints[0].x;
        Y_end = (Int32)screenPoints[0].y;
        graphic.DrawLine(Pens.Red, X_offset + X_start, Y_offset + Y_start, X_offset + X_end, Y_offset + Y_end);
    }

    void drawTriangle()
    {
        pictureBox1.Image = MyImage;
        graphic.Clear(Color.White);

        Points = new _3d_points();
        Points.x = new double[3];
        Points.y = new double[3];
        Points.z = new double[3];
        Points.depth = new double[3];
        Points.scaleFactor = new double[3];

        Points.x[0] = -100;
        Points.y[0] = -100;
        Points.z[0] = -100;
        Points.x[1] = 100;
        Points.y[1] = -100;
        Points.z[1] = -100;
        Points.x[2] = 0;
        Points.y[2] = 100;
        Points.z[2] = -100;
        Points.focalLength = 1000;
        Points.length = 3;

        double rx = trackBar1.Value;
        double ry = trackBar2.Value;
        double rz = trackBar3.Value;
        double d = trackBar4.Value;
        Points.depthScale = d;
        paint_3d_triangle(Points, 256, 256, rx / 1000, ry / 1000, rz / 1000);
    }

    void drawTriangle_i()
    {
        pictureBox1.Image = MyImage;
        graphic.Clear(Color.White);

        Points_i = new _3d_points_i();
        Points_i.x = new Int32[3];
        Points_i.y = new Int32[3];
        Points_i.z = new Int32[3];
        Points_i.depth = new Int32[3];
        Points_i.scaleFactor = new Int32[3];

        Points_i.x[0] = -100;
        Points_i.y[0] = -100;
        Points_i.z[0] = -100;
        Points_i.x[1] = 100;
        Points_i.y[1] = -100;
        Points_i.z[1] = -100;
        Points_i.x[2] = 0;
        Points_i.y[2] = 100;
        Points_i.z[2] = -100;
        Points_i.focalLength = 1000;
        Points_i.length = 3;

        Int16 rx = (Int16)trackBar1.Value;
        Int16 ry = (Int16)trackBar2.Value;
        Int16 rz = (Int16)trackBar3.Value;
        Int16 d = (Int16)trackBar4.Value;
        Points_i.depthScale = d;
        paint_3d_triangle_i(Points_i, (Int16)256, (Int16)256, (Int16)(rx), (Int16)(ry), (Int16)(rz));
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        for (UInt16 cnt = 0; cnt < sin.Count(); cnt++)
        {
            sin[cnt] = (Int16)(Math.Sin(Math.PI * cnt / 180) * 32767);
        }


        for (UInt16 cnt = 0; cnt < cos.Count(); cnt++)
        {
            cos[cnt] = (Int16)(Math.Cos(Math.PI * cnt / 180) * 32767);
        }
        drawTriangle_i();
    }

    private void trackBar1_Scroll(object sender, EventArgs e)
    {
        drawTriangle_i();
    }

    private void trackBar2_Scroll(object sender, EventArgs e)
    {
        drawTriangle_i();
    }

    private void trackBar3_Scroll(object sender, EventArgs e)
    {
        drawTriangle_i();
    }

    private void trackBar4_Scroll(object sender, EventArgs e)
    {
        drawTriangle_i();
    }
}

}

For double the X, Y, Z track bars need to have the range from - P*1000 to PI * 1000.
For integers the X, Y, Z track bars need to have the range from 0 to 360,

The sin and cos tables have the first 91 degrees sin and cos results in UInt16 representation.

The angle increment is 1 degree, but the affinity can be increased or decreased as needed.

This example is for a triangle, but can be implemented all combinations of lines, squares rectangles, hexagons, because the main function is working with points in space.