0

I'm attempting to draw celestial bodies moving around on simplified, perfectly circular orbits. I'm also drawing the projected orbital paths these objects will take. However, the problem is that the actual path the objects take doesn't agree with the projection on zooming in closely enough.

Video demonstrating the issue: https://www.youtube.com/watch?v=ALSVfx48zXw

If zoomed out, the problem is non-existent, because the deviation is too small. The apparent size of the deviation appears to be affected primarily by the visible curvature of the circles - notice how the paths of the moons agree with their motion. If one were to zoom in so that the moons' projected paths appear close to straight lines, they would have the same pattern of deviations as the planet shows.

Coordinates calculating methods:

double getX (long int time) {
    return orbit * cos(offset + time * speed);
}
double getY (long int time) {
    return orbit * sin(offset + time * speed);
}

Projected orbit drawing:

ellipse = scene->addEllipse(system.starX-body.orbit,
                            system.starY-body.orbit,
                             body.orbit*2,body.orbit*2,greenPen,transBrush); 

Drawing the celestial bodies where they actually appear:

ellipse = scene->addEllipse(-body.radius,
                            -body.radius,
                            body.radius*2,body.radius*2,blackPen,greenBrush); 
ellipse->setFlag(QGraphicsItem::ItemIgnoresTransformations);
ellipse->setPos(system.starX+body.getX(date2days(game.date)),
                system.starY+body.getY(date2days(game.date)));

How do I fix this so that the celestial bodies are always on the predicted curve?

EDIT1:
I have attempted using the suggested algorithm for drawing my own ellipse. The version adapted for use with Qt I reproduce here:

QPoint get_point(double a, double b, double theta, QPoint center)
{
    QPoint point;
    point.setX(center.x() + a * cos(theta));
    point.setY(center.y() + b * sin(theta));
    return point;
}

void draw_ellipse(double a, double b, QPoint center, double zoom_factor, QGraphicsScene * scene, QPen pen)
{
    double d_theta = 1.0d / zoom_factor;
    double theta = 0.0d;
    int count = 2.0d * 3.14159265358979323846 / d_theta;

    QPoint p1, p2;
    p1 = get_point(a, b, 0.0f, center);
    for (int i = 0; i <= count; i++)
    {
        theta += d_theta;
        p2 = p1;
        p1 = get_point(a, b, theta, center);

        scene->addLine(p1.x(),p1.y(),p2.x(),p2.y(),pen);
    }
}

The results weren't encouraging:

Not pretty

In addition to not looking pretty at zoom_factor 360, the application ran extremely sluggishly, using much more resources than previously.

EDIT2:
The improved version gives much better results, but still slow. Here is the code:

QPointF get_point(qreal a, qreal b, qreal theta, QPointF center)
{
    QPointF point;
    point.setX(center.x() + a * cos(theta));
    point.setY(center.y() + b * sin(theta));
    return point;
}

void draw_ellipse(qreal a, qreal b, QPointF center, qreal zoom_factor, QGraphicsScene * scene, QPen pen)
{
    qreal d_theta = 1.0d / zoom_factor;
    qreal theta = 0.0d;
    int count = 2.0d * 3.14159265358979323846 / d_theta;

    QPointF p1, p2;
    p1 = get_point(a, b, 0.0f, center);
    for (int i = 0; i <= count; i++)
    {
        theta = i * d_theta;
        p2 = p1;
        p1 = get_point(a, b, theta, center);

        scene->addLine(p1.x(),p1.y(),p2.x(),p2.y(),pen);
    }
}
Abu Dhabi
  • 311
  • 4
  • 13

2 Answers2

1

It appears that Qt does not auto-adjust the drawing precision or 'sampling resolution'.

You could try to draw the ellipse yourself, by drawing a loop of lines. Increase the sample resolution of the drawing when you zoom in - i.e. make the sampled points closer to each other.

Take the parametric equation of an ellipse

x = a cos (theta), y = b sin (theta)

where a and b are the semi-major and semi-minor axes of the ellipse, and sample the points with it:

(pseudo C++-style code)

point get_point(float theta, point center)
{
    return point(center.x + a * cos(theta), center.y + b * sin(theta));
}

void draw_ellipse(float a, float b, point center, float zoom_factor)
{
    float d_theta = 1.0f / zoom_factor;
    float theta = 0.0f;
    int count = 2.0f * PI / d_theta;

    point p1, p2;
    p1 = get_point(0.0f, center);
    for (int i = 0; i < count; i++)
    {
        theta += d_theta;
        p2 = p1;
        p1 = get_point(theta, center);

        drawline(p1, p2);
    }
}

Sorry if the code looks arbitrary (I'm not familiar with Qt), but you get the point.

  • This has nothing to do with infinite precision, merely with incorrectly chosen discretization step. It's essentially a bug in Qt. What you are offering is a possible workaround. – Kuba hasn't forgotten Monica Apr 07 '14 at 16:50
  • @KubaOber uh yeah, thanks for pointing that out - I meant he thought Qt automatically adjusts precision so the lines are imperceptible. –  Apr 07 '14 at 16:54
  • I have attempted to use this, with fairly miserable results - see my first edit above. – Abu Dhabi Apr 07 '14 at 19:08
  • @AbuDhabi pass your `pen` variable as a reference rather than a copy of the original object. Maybe you should implement a clipping/culling routine so you only draw the lines inside the screen. Look up the Sunderland-Hodgman clipping algorithm. –  Apr 07 '14 at 20:09
  • @willywonka_dailyblah Qt already does it for you, and the copy of a pen is cheap, so no sweat there. – Kuba hasn't forgotten Monica Apr 07 '14 at 20:10
  • @KubaOber hmm ok... there are probably other ways of speeding it up such as using sin/cos lookup tables and quadratic/cubic interpolation between the points... but probably too much complication for his goal... hehe... –  Apr 07 '14 at 20:14
  • sin/cos lookup tables are not very useful here, neither is interpolation. What is important is to draw a small number of visible segments only. That's what Qt does internally. – Kuba hasn't forgotten Monica Apr 07 '14 at 20:16
  • @KubaOber but if Qt does its job as best as it could, then the only possible bottleneck would be floating point calculations such as sin/cos –  Apr 07 '14 at 20:18
  • Qt internally does not calculate anything for invisible ellipse segments. When you reimplement ellipse rendering, to get the same performance you need to do similar optimizations :) – Kuba hasn't forgotten Monica Apr 07 '14 at 20:20
1

Assuming that all of the parameters you pass to addEllipse are of sufficient resolution, the issue seems to be with how Qt renders ellipses. The discretization used in ellipse drawing is not dependent on the transformation matrix of the view.

When a QGraphicsItem is being rendered in a view, its paint method certainly has access to the paint device (in this case: a widget). It could certainly determine the proper discretization step in terms of angle. Even if a graphics item were to render using regular painter calls, the painter has the same information, and the paint device certainly has this information in full. Thus there's no reason for Qt to do what it does, I think. I'll have to trace into this code and see why it fails so badly.

The only fix is for you to implement your own ellipse item, and chose the discretization step and begin/end angles according to the viewport size at the time of rendering.

qreal is a double - so that shouldn't be an issue unless Qt is configured with -qreal float.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Could it be, on the other hand, that my code is wrong and Qt's ellipse method is correct? It's hard to tell which one of them is being "jumpy" here. – Abu Dhabi Apr 07 '14 at 17:23
  • 1
    @AbuDhabi It's easy to check. Every time you update the location of the planet system, dump an accuracy score. Here's how to calculate it: For circular orbits, dump the distance from the sun divided by the radius of the outermost moon of the planet. For elliptical orbits, dump the sum of distances from the orbital foci to the planet, divided as above. This will let you gauge the jumpiness of the planet's location in understandable units of the outermost moon's orbital diameter. – Kuba hasn't forgotten Monica Apr 07 '14 at 18:02
  • Units of "pixels" are good enough for me, since that's the unit I use for the body attributes in this case. Anyway - the distance is always the same, according to this test. It seems rounded from the value I put into the body.orbit field, however (304.679144385027 vs 304.679). – Abu Dhabi Apr 07 '14 at 18:31
  • @AbuDhabi Pixels are the attributes of the view, not of the scene, so I don't quite know how you can get any particular pixels when you look at the scene and its items. – Kuba hasn't forgotten Monica Apr 07 '14 at 19:01
  • I assign body.orbit a value. This value is the distance from the sun, in pixels according to the default zoom level that the view starts with. I'm not sure why this is confusing. Also, I have attempted using willywonka_dailyblah's algorithm, and results are not encouraging. Any idea why? – Abu Dhabi Apr 07 '14 at 19:15