Planetary Cloud Shader

Hey!

First a bit of background.
I’m a huge Elite and space sim fan! Sometimes i get cravings to play First Encounters again, but usually when i do, i get so disappointed by how primitive it is nowadays and don’t get very far.
A while back i got this urge again, and decided to check what else is out there.
I found this: http://pioneerspacesim.net/
An open source “clone”, or something similar, of First Encounters. Rather than playing it, i went on and checked some youtube vids of someone playing it. The guy flew between star systems and landed on planets, watching sunrises and sunsets. It looked quite nice, nice atmospheric shader and all. But the worlds were barren. Pretty natural, i guess, most worlds are barren. But it was something else as well, there were never any clouds. This felt wrong. If there’s a decent atmosphere (which some planets claimed to have), there should at least be hints of clouds sometimes.
This annoyed me, and i started contemplating the matter. I wanted to do planet stuff.

The other day, i stumbled across @aaronperkins: http://hub.jmonkeyengine.org/forum/topic/jmeplanet/
I’ve been watching that video several times now, being equally impressed each time (not saying how much i’d like to try it in my non-delivered Oculus Rift).

An excellent start for some kind of space simulation.
Still, no clouds!

So i thought, what would it take to add clouds to a planet like that?

What i came up with was this (not saying i invented it, i’m pretty sure it’s a common way to create clouds. There might even be something like this on the forum).

I made a custom shader, with two textures.
One has three layers of clouds, sparse, medium and dense/overcast stored in the r,g and b channels.
The shader selects these using either a second rgb texture, or vertex colors (in case you’d like something more flexible).

I started to struggle a bit with uploading it to bitbucket, but it seems they don’t like subversion. I can post the code and textures somewhere in case anyone wants to implement it.
http://youtu.be/fYQqJI8uQ0k

Clouds.j3md
MaterialDef Colored Textured {
MaterialParameters {
Texture2D Clouds
Texture2D Selector
Boolean VertexColor (UseVertexColor)
Boolean SelectFromTexture
Float LowerAlphaThreshold : 0.35
Float UpperAlphaThreshold : 0.6
Int CloudTextureWidth : 512
Int SelectorWidth : 256
Float WindSpeed : 0.01;
}

        Technique {
            LightMode MultiPass
            VertexShader GLSL100:   MatDefs/Clouds.vert
            FragmentShader GLSL100: MatDefs/Clouds.frag

            WorldParameters {
                WorldViewProjectionMatrix
                WorldViewMatrix
                ViewMatrix
                NormalMatrix
                Time
            }

            Defines {
                USE_VERTEXCOLOR : VertexColor
                USE_SELECTOR_TEX : SelectFromTexture
            }
        }
    }

Clouds.vert

uniform mat4 g_WorldViewProjectionMatrix;
uniform mat4 g_WorldViewMatrix;
uniform mat4 g_ViewMatrix;
uniform vec4 g_LightColor;
uniform vec4 g_LightPosition;
uniform mat3 g_NormalMatrix;
uniform vec4 g_AmbientLightColor;

attribute vec3 inPosition;
attribute vec2 inTexCoord;
attribute vec3 inNormal;

varying vec2 texCoord;
uniform float g_Time;
varying float time;

varying vec4 ambientColor;
varying vec4 lightColor;
varying vec4 vLightDir;
varying vec3 vNormal;
varying vec3 vViewDir;
varying float inside;
//uniform sampler2D m_Selector;

#ifdef USE_VERTEXCOLOR
    attribute vec4 inColor;
    varying vec4 vertColor;
#endif

void lightComputeDir(in vec3 worldPos, in vec4 color, in vec4 position, out vec4 lightDir){
    float posLight = step(0.5, color.w);
    vec3 tempVec = position.xyz * sign(posLight - 0.5) - (worldPos * posLight);
    lightDir = vec4(normalize(tempVec), 1.0);
}

void main(){
    texCoord = inTexCoord;
    time = g_Time;
    gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
    vec3 wvNormal  = normalize(g_NormalMatrix * inNormal);
    vec4 wvLightPos = (g_ViewMatrix * vec4(g_LightPosition.xyz,clamp(g_LightColor.w,0.0,1.0)));
    wvLightPos.w = g_LightPosition.w;

    lightColor = g_LightColor;
    vec3 wvPosition = (g_WorldViewMatrix * vec4(inPosition, 1.0)).xyz;
    lightComputeDir(wvPosition, lightColor, wvLightPos, vLightDir);

    lightColor.w = 1.0;
    vNormal = wvNormal;
    vViewDir = normalize(-wvPosition);
    #ifdef USE_VERTEXCOLOR
        vertColor = inColor;
    #endif
    float len = length(g_CameraPosition - inPosition.xyz);
    inside = len;

    ambientColor = vec3(0.2, 0.2, 0.2) * g_AmbientLightColor.rgb;
}

Clouds.frag
varying vec2 texCoord;
uniform sampler2D m_Clouds;
uniform sampler2D m_Selector;
uniform float m_LowerAlphaThreshold;
uniform float m_UpperAlphaThreshold;
uniform int m_CloudTextureWidth;
uniform int m_SelectorWidth;
uniform float m_WindSpeed;
varying float time;

varying vec3 ambientColor;
varying vec4 lightColor;
varying vec4 vLightDir;
varying vec3 vNormal;
varying vec3 vViewDir;
#ifdef USE_VERTEXCOLOR
    varying vec4 vertColor;
#endif

vec2 computeLighting(in vec3 wvNorm, in vec3 wvLightDir){
    float diffuseFactor = max(0.0, dot(wvNorm, wvLightDir));
    float specularFactor = 0.0;//lightComputeSpecular(wvNorm, wvViewDir, wvLightDir, m_Shininess);

    float att = vLightDir.w;

    return vec2(diffuseFactor, specularFactor) * vec2(att);
}



void main(){
    float t = time * 0.1;
    float y = mod(t / m_CloudTextureWidth, m_CloudTextureWidth);
    float x = mod((t / m_CloudTextureWidth - y + t * m_WindSpeed), m_CloudTextureWidth);
    vec4 cloud = texture2D(m_Clouds, vec2(texCoord.x + x, texCoord.y + y));
    vec4 pick;
    vec4 cloud2;
    #ifdef USE_VERTEXCOLOR
        pick = vertColor;
    #else ifdef USE_SELECTOR_TEX
        t *= 10;
        float y2 = mod(t / m_SelectorWidth, m_SelectorWidth);
        float x2 = mod((t / m_SelectorWidth - y2), m_SelectorWidth);
        
        pick = texture2D(m_Selector, vec2(texCoord.x + x2, texCoord.y + y2));
        
    #endif
    float c1 = cloud.r * pick.r;
    float c2 = cloud.g * pick.g;
    float c3 = cloud.b * pick.b;
    float prod = c1 + c2 + c3;
    prod = min(prod, 1.00);
    vec4 lightDir = vLightDir;
    lightDir.xyz = normalize(lightDir.xyz);
    vec3 normal = vNormal;// * vViewDir; // vec3(0.0, 1.0, 0.0);//
    vec2 light = computeLighting(normal, lightDir.xyz);

    float threshold = m_UpperAlphaThreshold - m_LowerAlphaThreshold;

    prod = min(1.0, prod);
    if(prod < m_LowerAlphaThreshold){
        prod = 0.0;
    } else if(prod > m_UpperAlphaThreshold){
        prod = 1.0;
    } else {
        prod = prod / threshold - m_LowerAlphaThreshold / threshold;
    }
    gl_FragColor = vec4(vec3(prod * light.x ) + ambientColor.rgb, prod * pick.a);
}

Clouds.j3m
Material My Material : MatDefs/Clouds.j3md {
MaterialParameters {
Selector : Repeat Textures/Selection.jpg
Clouds : Textures/Clouds.png
}
AdditionalRenderState {
Blend Alpha
FaceCull Off
Wireframe Off
}
}

TestCode:
public void simpleInitApp() {
flyCam.setMoveSpeed(40);
Sphere cloudSphere = new Sphere(50, 50, 105.2f);

        cloudSphere.setTextureMode(Sphere.TextureMode.Polar);
        Geometry cloudGeom = new Geometry("Clouds", cloudSphere);
        
        Material cloudMat = assetManager.loadMaterial("Materials/Clouds.j3m");
        //cloudMat.getAdditionalRenderState().setAlphaTest(true);
        
        cloudGeom.setMaterial(cloudMat);
        cloudGeom.setQueueBucket(RenderQueue.Bucket.Transparent);
        cloudMat.setBoolean("SelectFromTexture", true);
//        cloudMat.setBoolean("VertexColor", true);
//        float[] colors = new float[cloudSphere.getVertexCount() * 4];
//        
//        for(int i = 0; i < colors.length; i+= 4){
//            colors[i] = 0.0f;
//            colors[i+1] = 0.0f;
//            colors[i+2] = 1.0f;
//            colors[i+3] = 1f;
//        }
//        cloudSphere.setBuffer(VertexBuffer.Type.Color, 4, colors);
//        cloudSphere.updateCounts();
        rootNode.attachChild(cloudGeom);
        Sphere b = new Sphere(20, 20, 100);
        Geometry geom = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        
        geom.setMaterial(mat);

        rootNode.attachChild(geom);
        
    }
4 Likes

I hoped to see something related to spherical horses moving through vacuum, but the planet is nice :wink:

Two gripes, though:

  1. Since i use “Polar” texture mode, i get some stretching around the equator. Is there a solution for this?

  2. The clouds are flat, so not very exciting to fly through. They look best from afar.

As you get closer you would need to swap in higher detail levels of the clouds.

Have you seen Elite Dangerous? If you liked Elite you should check it out.

Yes, the asteroids video from Elite Dangerous was the trigger that made me look for planet stuff in jme :slight_smile:

Downloaded jmeplanet and applied the clouds there. Haven’t been able to make it play well with the atmosphere yet, though.

1 Like

Added lighting and time to the shader, so now it animates as well. Video in topic

1 Like

Very nice!

Very cool work!

This would be a great addition to jmeplanet. When it gets to a stable point, let me know, and I’ll throw it in.

Updated code and video.
Added support for alpha clamping, clouds are now more concentrated and configurable, play with the uniforms to change how much cloud there is. It’s also faster due to there being less alpha transitions around.
Tweaked the textures a bit.
Added ambient light.

It still looks a bit crap around the poles. Is it worth tweaking the UV texturing on the sphere, or does it need triplanar mapping to look good?

Works ok with jmeplanet now, but the clouds need to be outside the ‘atmosphere’ and you can see the transition.

I deem it “good enough” for now, and probably won’t spend too much more time on this, right now.