Defenestrate the bifurcators!

Vertex projection, spherical regions

This update is a bit late; usually I try to make commits as small as possible, but this time I spent more than a week in uncommitable state (something didn’t compile, run or there was a lot of temporary code). Most work was done on drawing of spherical regions and projection of vertices. That said, I’m mostly done with some of the most problematic code.

Dependencies, dependencies

StelSkyDrawer, which I originally expected to be difficult, turned out to be rather simple to get done. I continued to replace current graphics code with code based on the new Renderer subsystem, and then I ran into the StelPainter::drawSphericalRegion() function, which turned out to be a major challenge due to a massive chain of dependencies on other code.

drawSphericalRegion() has many different code paths depending on its parameters such as draw mode (outline, fill, textured-fill) or whether to subdivide drawn triangles. These branch off to other functions, mainly in StelPainter, some of which depend on SphericalRegion functions, and through that, OctahedronPolygon. Some of these branches need StelProjector to project vertices before drawing, while some use more complex logic, also involving StelProjector (the subdividing code paths do the projection while subdividing).

drawSphericalRegion() and its callees operate on a pointer to a SphericalRegion, which could poin to any one of multiple classes derived from SphericalRegion, but based on these flags they make assumptions about implementation details of the spherical region (e.g. whether or not it has texture coordinates), even though it isn’t known which particular class is being used. Because of this, adding a new SphericalRegion class might break the code as it wouldn’t necessarily fit all these assumptions.

The new Renderer subsystem API is designed to be as minimal as possible. This helps both users (less documentation to read) and implementers (backends can be simpler). Ideally, all drawing should be done through one function (drawVertexBuffer()). It is useful to define some convenience functions; e.g. drawRect(), which internally uses drawVertexBuffer() and doesn’t require backends to implement it. However, unlike drawRect(), which is used in various ways, drawSphericalRegion() is only used in combination with a SphericalRegion, so it made more sense for me to move it there as a function, built on top of the Renderer API.

Additionally, a SphericalRegion member function can have different versions for each spherical region class, removing the problem with assumptions about implementation details. Any new spherical region class would simply need to override drawing functions.


So I started rewriting drawSphericalRegion() as SphericalRegion::draw(), using the Renderer API. Then I found that I also need to reimplement half of StelPainter and various other code. I made some changes to simplify this.

Code for boundary and and fill draw modes was completely different, and so I separated it into two methods, drawFill() and drawBoundary()/drawOutline() (the latter is not implemented yet). Also, textured fill draw mode is only really used with spherical region classes supporting texturing, while normal fill mode is used with others. However, drawSphericalRegion() could be called with a SphericalRegion without texturing support in combination with textured fill mode, which doesn’t make sense. Due to this I removed the draw mode parameter completely, turning texturing into an implementation detail. That is, if a spherical region class needs texturing, it overrides the draw functions to support it. If we evever need to draw SphericalRegions with texture support without textures, a simple enableTexture()/disableTexture() flag could be added either to the SphericalRegion derived class or to StelRenderer.

Much of the code would be shared between classes in SphericalRegion hierarchy, so I implemented drawFill() as a non-virtual function containing common code calling overridable functions (like vertex buffer caching and drawing). Also, other StelPainter code used by drawSphericalRegion() ended up in SphericalRegion, e.g. triangle subdivision.

Every time a spherical region is drawn, all vertices must be processed, either when subdividing (which invloves projection - outside Renderer) or when projecting vertices (on Renderer side). As the vertex buffer backend might store vertex data in any way (e.g. on the GPU), we add one more copy of each vertex, causing even more overhead. I tried to decrease these costs by caching vertex buffers ready for drawing based on drawing arguments, but this could be done only in few cases (in particular, we can’t cache when subdividing triangles, as subdivision happens together with projection, and we can’t really determine whether the projector (StelProjector) has changed or not.).

The only real solution is to handle projection on the GPU with shaders, which would enable storing vertex data in VRAM in a vertex buffer object. However, that can’t be done without writing all projection code twice as long as we support both GL1 and GL2 (since GL1 still needs to do it on the CPU). One way to do this this is to force all StelProjector classes to have a GLSL implementation (as well as one for any other shading language that might be supported in future). Another is to expose shaders if supported, which in turn would require code outside renderer to have two (or more, for more shading languages) separate paths. I’m not doing this in this project - it is a large project by itself, but if needed, it should be possible to do it after the refactor. Handling projection on the GPU should drastically (order of magnitude?) improve performance as we wouldn’t need to send the data to the GPU with each frame.

I also wrote a vertex buffer “cache” that would let Renderer manage some vertex buffers’ deallocation instead of user code. This is because some code in OctahedronPolygon used vertex arrays in static variables, but vertex buffers can’t really be static - they need a Renderer to be initialized and destroyed (as their backend might be implemented as a VBO, or another not-in-RAM way). With this cache, a static object would have a vertex buffer ID that lazily initialized to point to a buffer once a Renderer is available, with buffer being destroyed and ID invalidated at Renderer destruction. The code was not needed in the end, though, so for now it isn’t committed. It might be useful later, though.

StelProjector and vertex buffers

Non-subdividing SphericalRegion drawing code path needed to project vertices before drawing with StelProjector, which is done in Renderer backend (previously in StelPainter). It could be done outside, but doing it internally enables some optimizations (this feature in particular causes considerable overhead). Until now, this was stubbed in backend code for drawVertexBuffer(). That said, we still only have the vertex array based vertex buffer backends (same principle as previous StelPainter code), so this is pretty much same as in StelPainter.

This code is not yet used/tested, though - it will be tested in following work.

Other changes

  • StelRenderer API:

    • Added new setBlendMode() function to set blend modes. Not as powerful as glBlendFunc() in OpenGL, but much more intuitive, with blend modes named as in image editing: multiply, add, none, alpha. Only blend modes that are actually used exist so backends don’t have to implement every posible blend mode (especially problematic in a software renderer). If needed, adding new modes is trivial.
    • Global color is now set in floating point, so we can set colors outside the 0-1 range.
  • StelVertexBuffer API:

    • New clear() function to clear vertices, allowing the backend to reuse previously allocated space (if possible) when adding new vertices.

    • Triangle fan, lines and line loop primitive types (couldn’t avoid using them).

    • Base class for all vertex buffer types, so we can have a pointer to any vertex buffer regardless of the vertex type it’s templated with. Mostly for internal Renderer usage, like vertex buffer caching.

    • Vertex type definitions are now much simpler.

      Previously, a vertex with a 3D position and color would be defined like this:

      struct SomeVertex
          // Vertex position in 3D.
          Vec3f position;
          // RGBA vertex color.
          Vec4f color;
          // Metadata describing vertex attributes (defined outside the class body).
          static const QVector<StelVertexAttribute> attributes;
      // This might need to be defined in a .cpp file to prevent multiple definition errors
      const QVector<StelVertexAttribute> SomeVertex::attributes =
          (QVector<StelVertexAttribute>() << StelVertexAttribute(AT_Vec3f, Position)
                                          << StelVertexAttribute(AT_Vec4f, Color));

      Now, it can be defined like this:

      struct SomeVertex
          // Vertex position in 3D.
          Vec3f position;
          // RGBA vertex color.
          Vec4f color;
          // Vertex attribute metadata.
          VERTEX_ATTRIBUTES(Vec3f Position, Vec4f Color);
  • Index buffer API and backend (only a plain array implementation so far). 16bit (default) or 32bit indices can be used.

  • We don’t set point size when drawing stars as points. This only works on very rare GPUs/drivers.

  • Generic triplet struct to replace static arrays of size 3 used to pass triangles, which have to be passed as plain pointers without array length (in C++11, this could be done with tuples).

  • Various minor cleanup, optimizations and bugfixes.