Thursday, November 10, 2011

Planetary Rings


What feels like a logical progression after my recent experiments with gas giant rendering, I thought I would next take a look a adding some rings to my planet, turning it into more of a Saturn than a Jupiter.  Reading up on them, Saturn's rings are a pretty complex structure the formation of which is not completely understood - as ever Wikipedia is a good starting point for information about them. Although they extend from 7,000 to 80,000 Km above the planet's equator they are only a handful of metres thick, being made up of countless particles of mainly water ice (plus a few other elements) from the truly tiny to around ten metres across.


Gas giant with rings as seen at dusk from a nearby planet
My approach for rendering such a complex system is similar in many ways to that for the gas giant itself, rather than ray-tracing though I opted for a more conventional geometric representation of the rings and used a single strip of geometry encircling the planet.  For now the rings are fixed in the XZ plane around the planet’s origin but as the planet itself can have an arbitrary axis in my solar simulation that doesn’t feel like too much of a limitation to be going on with.

Rendering the basic geometry produces this:

Basic ring geometry - a single tri-strip
What’s immediately apparent is once again we fall foul straight away of the polygonal nature of our graphics – although the tri-strip here is a pretty coarse approximation to a circle, no matter how many triangles we add to it we are still going to be able to see straight edges if we get close enough.  Fortunately the solution is simple and by adding a check to the pixel shader to reject pixels that are more than our maximum radius from the planet origin or less than our minimum we immediately get a nice smooth circular ring:

Clipping pixels by distance in pixel shader to produce smooth ring shape
The only trick here is to ensure the vertices of the ring geometry are far enough away from the planet origin that the closest distance of the edges of the triangles is at least the maximum ring radius rather than the vertices themselves being that distance otherwise the outer edges of the ring will be clipped by the geometry:
Vertices (in black) need to be further away than the ring radius to
prevent the ring (in red) being clipped by the triangle edges
A little trig shows this simply to be:


    vertRadius = ringRadius/cos(PI/numSides)


This only applies to the outer edge of the ring as on the inner edge by definition the triangle edges are closer to the centre than the vertices.


Another point to watch is the rings have to of course go both in front and behind the planet itself.  This sounds simple but is made slightly more complex that it first appears as I don't have a Z-buffer available when rendering at the planetary scale due to each planet being rendered in it's own co-ordinate space - they are simply rendered back to front - so they don't share a common Z space that could be used for a Z buffer.  The range of depths would be massive anyway and likely to cause flimmering artifacts.  Add on top of this the fact that planetary atmospheres need to alpha blend on top of whatever is behind them (including the far side of the rings) and a Z buffer isn't really an option.


Instead I render the rings in two passes, the first pass drawn before the encircled planet to provide the bulk of the rings and to allow the planet's atmosphere to alpha blend onto them.  The second pass is drawn after the planet has been rendered and uses an additional ray-sphere and stencil test to only render to the portion of the screen covered by the planet and only where the rings are in front of the planet surface.

Of course one great big slab isn’t very convincing so to get something a bit more realistic I again opted for a similar technique to the gas giant, I took a nice high resolution image of Saturn from the internet and cut out a slice of it’s rings, filtering the result down to a 1x2048 colour ramp texture.

Mapping this texture onto the ring geometry immediately produces something far more pleasing:


Colour ramp mapped onto ring geometry
The colour ramp doesn’t have any alpha information so looks wrong but with a little experimentation it turns out that calculating an alpha value in the pixel shader from the intensity of the colour works pretty well and also softens the inner and outer extremities of the ring:

Colour ramp mapped onto ring geometry with alpha from intensity
As the ring geometry has no thickness however, viewing it from acute angles produces some unsightly single pixel artifacts.  Although in no way scientific, pulling the Fresnel hammer out of the toolbox however makes the effect far less objectionable:

Rings edge-on without Fresnel term showing unsightly aliasing
Rings edge-on with acute Fresnel term - aliasing is reduced at the
expense of increased transparency
Note I’m using a pretty narrow Fresnel band for this purpose otherwise the ring tends to fade out too much of the time.  The number of rings in the effect can be varied with a simple scale value just like for the gas giant.

Changing the scale of the texture look-up to provide fewer, wider rings
More Noise

Although the rings here are pretty decent (largely due to stealing them from a real image I suspect), there is a distinct lack of detail when viewed from anything resembling close up.
Ultimately it would be cool to be able to fade in large numbers of actual meshes to represent the larger ice chunks that make up the rings when the view is near to or actually within the rings, but even without that I thought there was something that could be done to help represent the higher frequency detail these ice particles and ‘ice-asteroids’ present.

As with so many other procedural effects, I decided that a bit of noise would probably add some interest, so I added some code to sample the same noise texture I used for previous effects.  The problem here though is that the particle detail in the rings is very high frequency in the cosmic scale of things but from a purely visual basis I wanted to show variety in the rings from all ranges.

To do this I re-used a trick from the terrain texturing shader where the scale of the texture co-ordinates is calculated on the fly using the partial derivative of the actual texture co-ordinates from the pixel - in this case from the position of the point being shaded on the plane of the rings.  The texture co-ordinates are scaled up or down in powers of two to produce as close to screen size texels as possible - in fact two scales are used by rounding the perfect scale up and down to the closest power of two then blending between these two texture samples to produce a smooth transition.

Ring noise sampled at uniform texel density produces aliasing and tiling at distance
Ring noise sampled at adaptive density to provide more uniform screen coverage without tiling
The texture co-ordinate effect you can see here is somewhat akin to mip-mapping but instead of using a lower resolution version of the texture to represent the same area of surface at a larger distance, I am instead using the same resolution texture to represent a larger area at that distance thereby eliminating the tiling effect often seen with high frequency textures at range.  Unlike mip-mapping it also works in the opposite direction, using the same resolution texture to represent smaller and smaller surface areas as it gets closer to the camera and each texel covers more than one pixel.

Using this signed noise value to lighten or darken each shaded point produces a nice fine grain effect in the rings I hope is vaguely representative of the countless millions of icy particles that in fact make them up.

Rings without noise
Rings with noise to simulate particulate detail
Lighting

As with everything else in my little solar system the rings need to be illuminated by the Sun, this is done simply using standard dot product lighting but the little twist here is to use the noise value from the previous step to slightly perturb the normal uses for the calculation.  This is a gross simulation of the arbitrary facets of the lumps of ice making up the rings being illuminated by the Sun and simply adds a bit of randomisation to the lighting.

A more dramatic effect which adds solidity to the rings is the shadow of the planet cast on to them.  Rather than using shadow mapping techniques this is calculated by doing a ray intersection in the pixel shader from the pixel towards the sun.  To soften the ubiquitous hard edge produced by ray-traced shadows the distance the ray travels through the planet is used to provide a soft edge where the distance is small falling off quickly to solid shadow.  While not strictly physically accurate when classically treating sunlight as a parallel source, I feel it adds to the effect.


Shadow of the gas giant cast onto the rings
Close up of edge of shadow showing the simulated penumbra
Finally the colours in the rings can be remapped again using the same RGB vector system I used before so each final colour component is a dot product of the shader output and the modulation vector passed in to the shader for that channel

Colour re-mapped rings
More drastically colour re-mapped rings :-)
And that's about it.  Any planetary body in my simulation can now have rings of arbitrary radii although it's probably best to not go crazy with them to keep some sense of reality.  Future work for them might include the inclusion of a particle system to provide a sense of thickness as the camera passes through the ring and/or even better a mesh particle system so you can see individual large ice chunks spinning away in space.
For now though, I'll wrap up with another couple of views of my gas giant's rings, comments as always are welcome!


Gas giant with rings from orbit of nearby planet
The same gas giant from nearer the surface of the planet

12 comments:

  1. Absolutely breathtaking. I personally can't even get atmospheric scattering to work so I am always blown away by something like this.

    ReplyDelete
  2. Great work! I'm really enjoying your posts on star/planet rendering.

    ReplyDelete
  3. I've been working on a project similar to yours for a few years now while I'm in school. I don't have nearly the skill you have, so I hope you don't mind if I pick your brain occasionally.

    I can't seem to figure out how to do the stenciling you're describing for the rings. When I render the planet to the stencil buffer, it cuts out both the rings in front and back. How do you get it so it only stencils out the ring section behind the planet?

    ReplyDelete
  4. Hi Schiavo, the rings are in fact rendered twice:

    1. Render the rings without writing to the stencil buffer
    2. Render the planet and it's atmosphere writing a non-zero value into the stencil buffer. The planet overwrites the rings and the atmosphere alpha-blends into them
    3. Render the rings again using stencil test to only render where the planet wrote non-zero into the stencil buffer

    This does cause pixels for the portion of the rings covered by the planet to be rendered twice but I can live with that. A ray could be intersected from the ring to the viewpoint in the ring pixel shader to see if it fell behind the planet which would allow pixels that would ultimately be overwritten to not be shaded in the first pass, but I suspect the ray-sphere intersection would largely cancel out any saving in shading cost.

    ReplyDelete
    Replies
    1. I understood that much. What I wasn't understanding is why redrawing the rings anywhere the planet wrote non-zero to the stencil buffer won't redraw the portion of the rings behind the planet.

      Here's the rings when the stencil op is set to equal (reference is 0), so it's drawing wherever the planet isn't: http://i.imgur.com/t9vaC.png

      And here's the rendering with the stencil op set to notequal, so it's drawing where the planet is: http://i.imgur.com/gMy2z.png

      Those are both from the second rendering pass. How did you use the stencil to only draw the portion of the rings in front? The way I'm visualizing it (and seeing it rendered) is that both the segments in front and back get rendered again because they are both in an area where the stencil is non-zero.

      Sorry I'm not quite grasping this. I must be missing one small detail.

      Delete
    2. Ah, you are right I did miss out one vital detail for that situation. When the rings are rendered in the second pass the pixel shader used does a ray-sphere intersection test to determine whether the pixel should be rendered or not.

      It tests the ray from the pixel being shaded to the viewpoint against the sphere of the planet and where there is an intersection uses the HLSL clip() instruction to discard the pixels that are behind the sphere.

      Delete
    3. Thanks for your time, you're being extremely helpful.

      Everywhere I've checked for ray-sphere intersection tests has a different formula. I've tried about 10 of them in my pixel shader so far, using the camera position (start of ray) as the origin, and the position of the center of the sphere relative to the camera.

      Some of the formulas result in all the pixels being rendered, and others clip them all. So far, I haven't found a formula that is correct.

      What are you using?

      Delete
  5. For this situation I use a simple little intersection function:

    float IntersectRaySphere(
    const float fSphereRadiusSquared, // Squared radius of the sphere to intersect against
    const float3 v3RayOrigin, // Position of the ray origin
    const float3 v3RayUnitDirection, // Unit length vector representing the ray's direction away from the origin
    )
    {
    const float fB = dot(v3RayOrigin, v3RayUnitDirection);
    const float fD = sqrt((fB*fB) - (dot(v3RayOrigin, v3RayOrigin)-fSphereRadiusSquared));
    return -fB-fD;
    }

    Which returns the distance to the first intersection point. I compare this distance to the distance from the camera to the pixel on the ring being shaded and reject the pixel if the intersection distance is less (i.e. the ray hits the planet sphere before it would hit the ring):

    clip(IntersectRaySphere(m_fPlanetRadiusSquaredKm, m_v3ViewPosition, v3RayUnitVector)-fViewToPixelDistance);

    hope that helps.

    ReplyDelete
  6. A little late to the party here, but couldn't you accomplish the same thing by using a quad, rather than the more complicated ring geometry? The outer and inner cutoff would be nearly identical, with less geometry.

    ReplyDelete
    Replies
    1. An interesting idea...true there would be fewer vertices to send to the GPU but the number of pixels being processed by the pixel shader would increase significantly as there would be a lot more screen area covered by a single large quad.

      Even though the pixel shader would be able to reject most of them quickly with the range tests, on balance I think sending a couple of dozen extra vertices through the relatively cheap vertex shader is probably more efficient than having thousands more pixels sent to the pixel shader many of which have no effect on the final image.

      Empirical testing would be required to prove it one way or the other of course.

      Delete
    2. This method is significantly better for mobile, where the blank "corners" of a quad would cause overdraw (say, if the use case was a screen full of circles). For a single ring, I would agree with JeBuS.

      Delete

Comments, questions or feedback? Here's your chance...