Moving on from star rendering in my continuing tour of
celestial bodies, I thought it would be interesting to have a go at trying to
procedurally render a different type of entity – Gas Giants.
There are four Gas Giants in our own Solar System, Jupiter, Saturn, Uranus and Neptune each very different from the
other, but for my first go at rendering one I decided on the largest of these –
namely Jupiter – to see how close to it’s distinctive appearance I could manage
at real-time rates.
There is a lot of information available about Jupiter (http://en.wikipedia.org/wiki/Jupiter)
and Gas Giants in general (http://en.wikipedia.org/wiki/Gas_giant)
on sites such as Wikipedia, again the more I found out about the subject the
more interesting it became and I strongly recommend a little background reading
if it’s a subject that has any more than a passing interest for you.
Procedural Gas Giant generated and rendered with Osiris |
Composite Jupiter image from the Cassini-Huygens probe (NASA/JPL/Arizona University) |
Like stars Gas Giants are conveniently smooth for rendering
so I decided to use the same ray-tracing approach that I used for stars
described in my previous posts. This is
not only efficient in terms of geometry but also produces lovely smooth spherical
surfaces at all screen sizes which is important as I want to be able to fly
right down near to the surface of my planets.
I also wanted to be able to produce a wide variety of gas
giant effects with only a few input parameters so needed a system that could
make interesting visual surfaces with more variation than just their colour.
As seen in the images above, gas giants tend to follow a
basic structure of having bands of different coloured material at differing
latitudes; these are counter-circulating streams of material called zones and belts and trying to simulate such a major feature seemed like a good starting point. Using the planet space ‘y’ co-ordinate of the
pixel being shaded (essentially the latitude) as a lookup into a colour ramp texture is a nice simple
starting point for this effect. In this
case I generated the (1 x 2048) sized look up texture by taking a slice out of
the above cassini image of Jupiter and filtering it down to the correct size –
this gave me a genuine Jovian colour palette straight off-bat, support for
other colours is described below.
Basic colour bands from look-up texture |
Using Noise
Noise channel #1 |
Noise channel #2 |
Noise channel #3 |
Noise channel #4 |
Combined noise |
Colour ramp rendered with noise |
This looks pretty decent from this fairly distant viewpoint
but as the camera moves in towards the surface the pattern loses detail as it’s
stretched out over more screen space.
Although the naturally soft nature of the surface detail makes this far
less unsightly than it would be on a planet surface with more innate contrast,
I thought it could be improved by adding additional octaves of progressively
higher frequency noise.
The problem with adding higher frequencies
however is that you start to get under-sampling artifacts showing up as sparkly
noise in the effect when viewed from too large a distance for that
frequency - an almost classic example of shader aliasing. To avoid this two blend
weights are calculated based upon a combination of the distance of the planet
from the camera and it’s radius. These
two blend weights are passed to the gas giant pixel shader and used to control
the contribution of the upper two bands of noise frequencies. In this way the higher frequency noise blends
in smoothly as the camera nears the surface and fades out as it moves further
away. By choosing the blend distances
appropriately the transition is essentially invisible.
Close in to gas giant surface without high frequency noise |
Close in to gas giant surface with high frequency noise |
Silhouette Edges
While the plan is to add anti-aliasing in the future (either MSAA if performance allows or possibly FXAA) I realised there is something that can be done in the shader itself to improve matters and provide some other desirable effects. Rather than having a hard edge around the sphere it would be far nicer to have a thin band of alpha pixels fading off around the edge to smooth things out.
Fortunately this is quite simple to do, by using
a Fresnel term calculated with a dot product of the surface normal and the ray
from the camera to the pixel being shaded the alpha value of the pixel can tend
to zero as the surface becomes tangential to the view direction. The only trick required is to control the
amount of Fresnel to use to give a consistent on-screen width of just a couple
of pixels for the fall off zone – without this the alpha zone would grow and
shrink in size as the camera changed distance.
The Fresnel zone is calculated once in C++ and passed to the shader as a
constant.
Planet silhouette edges without Fresnel based alpha - aliasing very evident |
Planet silhouette edges with Fresnel based alpha - much smoother |
A handy side effect is that by clamping the distance factor
a larger fade out zone can be used when the camera nears the planet surface
producing a completely non-scientific but rather nice atmosphere effect to
soften the horizon at these close-up ranges - this can be seen in the high frequency noise planet close up images above.
Cyclones
Adding noise certainly made the surface of my gas giant more
interesting but I wasn’t satisfied, it still wasn’t quite what I had hoped
for. Reading about gas giants and
looking at the Cassini image at the top of this post it’s quickly becomes
obvious that a primary feature of these planets are large numbers of cyclones
and anti-cyclones ranging in size from barely perceptible to ones such as the
“Great Red Spot” on Jupiter several times larger than the Earth itself
combining to make for a highly turbulent surface.
Modelling planet scale cyclones accurately is a computationally expensive proposition beyond the capabilities of a real time system but there is something that can be done on a purely visual level to give at least a nod towards their presence. To do this I decided to model a system where a potentially large number of cyclones could be represented as a set of cones emanating from the centre of the planet each with their own radius and rotational strength. In the pixel shader the point being shaded is tested against this set of cones and should it fall within one is then rotated around that cone’s axis by the cone’s rotational strength scaled by the points distance from the cone’s central axis.
Testing a point against potentially hundreds of cones in the pixel shader is however a whole heap of processing so an efficient way to do so is required. The method I decided upon was to store the cone information in a relatively low resolution cube map texture that could be sampled using the normal of the planet sphere at the point being shaded to determine which cone the point in question fell within, if any. This way a single texture read provides all the information without having to iterate over each cone individually in the shader.
The cyclone cube map texture uses an uncompressed 32bit format encoded as follows:
Modelling planet scale cyclones accurately is a computationally expensive proposition beyond the capabilities of a real time system but there is something that can be done on a purely visual level to give at least a nod towards their presence. To do this I decided to model a system where a potentially large number of cyclones could be represented as a set of cones emanating from the centre of the planet each with their own radius and rotational strength. In the pixel shader the point being shaded is tested against this set of cones and should it fall within one is then rotated around that cone’s axis by the cone’s rotational strength scaled by the points distance from the cone’s central axis.
Testing a point against potentially hundreds of cones in the pixel shader is however a whole heap of processing so an efficient way to do so is required. The method I decided upon was to store the cone information in a relatively low resolution cube map texture that could be sampled using the normal of the planet sphere at the point being shaded to determine which cone the point in question fell within, if any. This way a single texture read provides all the information without having to iterate over each cone individually in the shader.
The cyclone cube map texture uses an uncompressed 32bit format encoded as follows:
- Red : X component of the unit length axis of the cone that encloses or is closest to this texel
- Green : Y component of the unit length axis of the cone that encloses or is closest to this texel
- Blue : Normalised rotational strength of the cyclone whos cone this texel is affected by
- Alpha : Normalised radius of the cyclone whos cone this texel is affected by, the sign of this value represents the sign of the Z component of the unit length cone axis that is computed in the shader
The format is set up so each byte is interpreted by the GPU
as a signed normalised number so the values -128 to +127 are stored in the
texture bytes which then arrive in the range [-1, +1] when the texture is
sampled in the pixel shader.
The normalised rotational strength of the cyclone and normalised radius values encoded in the texture are scaled in the shader based upon minimum and maximum values passed to it in shader constants.
A little experimentation showed that between 100 and 200 cones of sensible radii could be effectively encoded using a cube map only 128x128 on each side, no two cones being allowed to be within one texel’s worth of angle to avoid filtering artifacts. A benefit of storing axis and radius information in the cube map rather than actual per-texel offsets is that oversampling artifacts in the texture sampling are avoided as the distance calculations in the shader are operating on the same axial values for every pixel affected by that cone – the down side is that each pixel can only be affected by a single cyclone but I am happy to live with that for now.
The normalised rotational strength of the cyclone and normalised radius values encoded in the texture are scaled in the shader based upon minimum and maximum values passed to it in shader constants.
A little experimentation showed that between 100 and 200 cones of sensible radii could be effectively encoded using a cube map only 128x128 on each side, no two cones being allowed to be within one texel’s worth of angle to avoid filtering artifacts. A benefit of storing axis and radius information in the cube map rather than actual per-texel offsets is that oversampling artifacts in the texture sampling are avoided as the distance calculations in the shader are operating on the same axial values for every pixel affected by that cone – the down side is that each pixel can only be affected by a single cyclone but I am happy to live with that for now.
Colour coding the pixels based upon the cone axis they are closest to and therefore affected by produces what is essentially a spherical Voronoi diagram of the points where the central axes of each cone penetrates the surface of the sphere
Voronoi diagram showing area of influence of each cyclone |
Colour coding the pixels instead with a greyscale value
indicating the distance from the central axis of their nearest cone produces
the following effect
Position and strength of each cyclone |
Clearly
indicating the size and position of each cyclone. Finally, using a combination of this distance
value and a global maximum rotational strength supplied to the shader in a
constant to rotate each sample point around it’s closest cone axis produces the
final cyclone effect:
Final Gas Giant with cyclones |
A number of cyclones of different size and strength are
easily visible here – it’s not perfect but I think it’s a worthwhile addition
that makes the overall effect more interesting.
Colours and Bands
The final step is to allow the shader to produce more varied
effects for the potentially large number of gas giants I want my procedural universe
to be able to contain. There are two
additional parameters that I’ve added to help support this increase in variety.
The first is a simple scale value that is used to control
the mapping of latitude values onto the colour ramp texture. By increasing the scale the width of the
bands can be reduced and their number increased while decreasing the scale of
course produces the opposite effect – fewer, wider bands
Gas Giant with twice as many bands |
Gas Giant with half as many bands |
The second is a colour remapping post-process for taking the
RGB generated by the shader and altering it to add variety. The manner I thought simplest to do this was
to pass three vectors to the shader that are multiplied by the red, green and
blue shader colour values immediately before the shader returns. For example passing (1, 0, 0), (0, 1, 0) and
(0, 0, 1) would leave the colour unchanged as the red output is just the red
input, the green output just the green input and so on.
Passing different values however completely changes the
result allowing the final red, green and blue components that end up on the
screen to be an arbitrary mix of those from the shader proper. Although any range could be used some care
needs to be taken to keep the general luminosity in a sensible range or we end
up with completely black or pure white planets. A degree of over-brightening can however produce some funky alien worlds.
Colour remapped gas giant |
More dramatically colour remapped gas giant |
Further variation could be achieved by using a different
noise texture, different noise octave
scales and weights or a different colour ramp texture but for now I think this
is sufficient.
Future work
Although the cyclone system was developed for gas giants,
there is no reason it couldn't also be applied to other bodies with
atmospheres. The clouds on my Earth
style planets for example are currently read from a fBm based cube map but it
would be interesting to see how they look with cyclones applied – something
else to put on my already lengthy TODO list I think.
No comments:
Post a Comment
Comments, questions or feedback? Here's your chance...