Real time atmospheric scattering shader -- Space view


#1

I finally did it! Real time atmospheric scattering from space! This method is based on Sean O’Neil’s work posted on his website and GPU Gems 2. This specific shader is only designed to be viewed from outside the atmosphere. This sample uses default camera controls, so the camera must be zoomed out first.

Uploaded with ImageShack.us

The only issue I found so far is that the atmosphere does not dissipate with altitude and quite white when viewed from the sun side. If anyone has any ideas on how to fix it I would love the input.

Python code. Requires the planet model found in the solar system sample as well as a sphere mesh with the normals inverted. You can use the planet model with a negative scale, but the atmosphere would be on the wrong side.

from pandac.PandaModules import loadPrcFileData

loadPrcFileData( '', 'frame-rate-meter-scale 0.035' )
loadPrcFileData( '', 'frame-rate-meter-side-margin 0.1' )
loadPrcFileData( '', 'show-frame-rate-meter 1' )
loadPrcFileData( '', 'window-title Atmosphere Demo' )
loadPrcFileData('', "sync-video 0")

from direct.directbase.DirectStart import *
from direct.task import Task
from panda3d.core import Shader, PointLight
import math
base.setBackgroundColor(0.0, 0.0, 0.0) 

light = render.attachNewNode( PointLight( "sunPointLight" ) )
light.setPos(1000,0,0)
render.setLight( light )
sun = loader.loadModel("planet_sphere")
sun.reparentTo(render)
sun.setPos(1050,0,0)
sun.setScale(5)

earth = loader.loadModel("planet_sphere")
earth.reparentTo(render)

# This is a sphere with the normals flipped
atmo = loader.loadModel("atmo")
atmo.reparentTo(earth)
atmo.reparentTo(render)
atmo.setScale(1.025)

outerRadius = abs((earth.getScale().getX() * atmo.getScale().getX()))
scale = 1/(outerRadius - earth.getScale().getX())
atmo.setShaderInput("fOuterRadius", outerRadius)
atmo.setShaderInput("fInnerRadius", earth.getScale().getX())
atmo.setShaderInput("fOuterRadius2", outerRadius * outerRadius)
atmo.setShaderInput("fInnerRadius2", earth.getScale().getX() * earth.getScale().getX())

atmo.setShaderInput("fKr4PI", 0.0025 * 4 * 3.14159)
atmo.setShaderInput("fKm4PI", 0.0015 * 4 * 3.14159)

atmo.setShaderInput("fScale", scale)
atmo.setShaderInput("fScaleDepth", 0.25)
atmo.setShaderInput("fScaleOverScaleDepth", scale/0.5)

# Currently hardcoded in shader
atmo.setShaderInput("fSamples", 3.0)
atmo.setShaderInput("nSamples", 3)

# These do sunsets and sky colors
# Brightness of sun
ESun = 15
# Reyleight Scattering (Main sky colors)
atmo.setShaderInput("fKrESun", 0.0025 * ESun)
# Mie Scattering -- Haze and sun halos
atmo.setShaderInput("fKmESun", 0.0015 * ESun)
# Color of sun
atmo.setShaderInput("v3InvWavelength", 1.0 / math.pow(0.650, 4), 
                                  1.0 / math.pow(0.570, 4), 
                                  1.0 / math.pow(0.475, 4))
                                  
atmo.setShaderInput("v3CameraPos", base.camera.getPos().getX(),
        base.camera.getPos().getY(), 
        base.camera.getPos().getZ())
# Light vector from center of planet.        
lightv = light.getPos() 
lightdir = lightv / lightv.length()
atmo.setShaderInput("v3LightPos", lightdir[0], lightdir[1], lightdir[2]) 
      
atmo.setShaderInput("fCameraHeight", base.camera.getPos().length())
atmo.setShaderInput("fCameraHeight2", base.camera.getPos().length()*base.camera.getPos().length())

atmo.setShaderInput("g", 0.90)
atmo.setShaderInput("g2", 0.81)
atmo.setShaderInput("float", 2)

atmoShader = Shader.load("shader.cg")
atmo.setShader(atmoShader)

def shaderUpdate(task):
    atmo.setShaderInput("v3CameraPos", base.camera.getPos().getX(),
        base.camera.getPos().getY(), 
        base.camera.getPos().getZ())
    atmo.setShaderInput("fCameraHeight", base.camera.getPos().length())
    atmo.setShaderInput("fCameraHeight2", base.camera.getPos().length()*base.camera.getPos().length())
    return task.cont

taskMgr.add(shaderUpdate, 'shaderUpdate')
run()

shader.cg

//Cg
// http://stainlessbeer.weebly.com/planets-9-atmospheric-scattering.html
// http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter16.html
// http://sponeil.net/
float g =-0.90f;
float g2 = 0.81f;
float fExposure =2;

float scale(float fCos)
{
    float x = 1.0 - fCos;  
    return 0.25 * exp(-0.00287 + x*(0.459 + x*(3.83 + x*(-6.80 + x*5.25))));

}
 
void vshader(float4 vtx_position:POSITION,
    uniform float4 k_v3LightPos,          // The light's current position
    in uniform float4 k_v3CameraPos,         // The camera's current position
    in uniform float4 k_v3InvWavelength,     // 1 / pow(wavelength, 4) for RGB
    in uniform float4 k_fCameraHeight,        // The camera's current height
    in uniform float4 k_fCameraHeight2,       // fCameraHeight^2
    in uniform float4 k_fOuterRadius,         // The outer (atmosphere) radius
    in uniform float4 k_fOuterRadius2,        // fOuterRadius^2
    in uniform float4 k_fInnerRadius,         // The inner (planetary) radius
    in uniform float4 k_fInnerRadius2,        // fInnerRadius^2
    in uniform float4 k_fKrESun,              // Kr * ESun
    in uniform float4 k_fKmESun,              // Km * ESun
    in uniform float4 k_fKr4PI,               // Kr * 4 * PI
    in uniform float4 k_fKm4PI,               // Km * 4 * PI
    in uniform float4 k_fScale,               // 1 / (fOuterRadius - fInnerRadius)
    in uniform float4 k_fScaleOverScaleDepth, // fScale / fScaleDepth
    in uniform float4 k_fScaleDepth,          // The scale depth (i.e. the altitude at which the atmosphere's average density is found)
    in uniform float4 k_fSamples,
    in uniform float4 k_nSamples,
    uniform float4x4 mat_modelproj,
    out float4 l_position : POSITION,
    out float4 l_color0   : COLOR0,
    out float4 l_color1   : COLOR1,
    out float3 l_v3Direction: TEXCOORD0 )
{
    float3 v3Pos = vtx_position.xyz;
    float3 v3Ray = v3Pos - k_v3CameraPos.xyz;
    float fFar = length(v3Ray);
 
    v3Ray /= fFar;
 
    // Calculate the closest intersection of the ray with the outer atmosphere 
    // (which is the near point of the ray passing through the atmosphere)    
    float B = 2.0 * dot(k_v3CameraPos.xyz, v3Ray);
    float C = k_fCameraHeight2.x - k_fOuterRadius2.x;
    float fDet = max(0.0, B*B - 4.0 * C);
    float fNear = 0.5 * (-B - sqrt(fDet));
     
    // Calculate the ray's start and end positions in the atmosphere,
    // then calculate its scattering offset 
    float3 v3Start = k_v3CameraPos.xyz + v3Ray * fNear;
    fFar -= fNear;
 
    // Initialize the scattering loop variables
    float fSampleLength = fFar / k_fSamples.x;
    float fScaledLength = fSampleLength * k_fScale.x;
    float3 v3SampleRay = v3Ray * fSampleLength;
    float3 v3SamplePoint = v3Start + (v3SampleRay * 0.5);
    
    float fHeight = length(v3SamplePoint);
    float fStartAngle = dot(v3Ray, v3Start) / k_fOuterRadius.x;
    float fStartDepth = exp(-1.0 / k_fScaleDepth.x);
    float fStartOffset = fStartDepth * scale(fStartAngle);
    
    // Now loop through the sample points 
    float3 v3FrontColor = float3(0, 0.0, 0.0);
    for(int i=0; i<3; i++)  
    {
        float fHeight = length(v3SamplePoint);
        float fDepth = exp(k_fScaleOverScaleDepth.x * (k_fInnerRadius.x-fHeight.x));
        float fLightAngle = dot(k_v3LightPos.xyz, v3SamplePoint) / fHeight;
        float fCameraAngle = dot(v3Ray, v3SamplePoint) / fHeight;
        float fScatter = (fStartOffset + fDepth * (scale(fLightAngle) - scale(fCameraAngle)));
        float3 v3Attenuate = exp(-fScatter *(k_v3InvWavelength.xyz * k_fKr4PI.x + k_fKm4PI.x));
        v3FrontColor += v3Attenuate * (fDepth * fScaledLength);
        v3SamplePoint += v3SampleRay;
    }
    l_color0.rgb = v3FrontColor.xyz * k_v3InvWavelength.xyz * k_fKrESun.x;
    l_color1.rgb=v3FrontColor*k_fKmESun.x;
    l_position=mul(mat_modelproj, vtx_position);  
    l_v3Direction =k_v3CameraPos.xyz - v3Pos;
}
 
void fshader(in float4 l_position : POSITION,
    in float4 l_color0   : COLOR0,
    in float4 l_color1   : COLOR1,
    in float3 l_v3Direction : TEXCOORD0 , 
    uniform float4 k_v3LightPos,
    out float4 o_color:COLOR)
{
    float fCos = dot(k_v3LightPos, l_v3Direction) / length(l_v3Direction);
    float fRayleighPhase = 0.75 * (1.0 + fCos*fCos);
    float fMiePhase= 1.5 * ((1.0 - g2) / (2.0 + g2)) * (1.0 + fCos*fCos) / pow(1.0 + g2 - 2.0*g*fCos, 1.5);
    //This causes a brightness reduction when viewing at 90 degrees
    //float4 col=(fRayleighPhase * l_color0) + (fMiePhase * l_color1);
    float4 col=(l_color0) + (fMiePhase * l_color1);
    col.a = col.b;
    //In some versions, not in others. Not sure what impact it has
    //o_color = 1 - exp(-fExposure * col);
    o_color = col;
}   

#2

you could multiply the result with 1 - dot product between view and surface normal. which would fade it out towards the sky. …at least i think it should.


#3

looks pretty good!


#4

I may have got this working a bit better, by fine-tuning the input parameters, and changing a bit of the shader code.
However, it only seems to work with the solar_sky_sphere model from the Solar-System sample. This could be due to the fact that I don’t know how to properly create .egg models. Also, the earth scale must be 1.0 and the atmo scale must be around 1.025. It fails at other sizes unfortunately (even when using the 2.5% ratio specified by O’Neil)

from pandac.PandaModules import loadPrcFileData 

loadPrcFileData( '', 'frame-rate-meter-scale 0.035' ) 
loadPrcFileData( '', 'frame-rate-meter-side-margin 0.1' ) 
loadPrcFileData( '', 'show-frame-rate-meter 1' ) 
loadPrcFileData( '', 'window-title Atmosphere Demo' ) 
loadPrcFileData('', "sync-video 0") 

from direct.directbase.DirectStart import * 
from direct.task import Task 
from panda3d.core import Shader, PointLight 
import math 
base.setBackgroundColor(0.0, 0.0, 0.0) 

light = render.attachNewNode( PointLight( "sunPointLight" ) ) 
light.setPos(1000,0,0) 
render.setLight( light ) 
sun = loader.loadModel("models/solar_sky_sphere") 
sun.reparentTo(render) 
sun.setPos(1050,0,0) 
sun.setScale(5) 

earth = loader.loadModel("models/planet_sphere") 
earth.reparentTo(render) 

# This is a sphere with the normals flipped 
atmo = loader.loadModel("models/solar_sky_sphere") 
#atmo.reparentTo(earth) 
atmo.reparentTo(render) 
atmo.setScale(1.025)


#outerRadius = abs((earth.getScale().getX() * atmo.getScale().getX()))   # ?????
outerRadius = atmo.getScale().getX()
scale = 1/(outerRadius - earth.getScale().getX()) 
atmo.setShaderInput("fOuterRadius", outerRadius) 
atmo.setShaderInput("fInnerRadius", earth.getScale().getX()) 
atmo.setShaderInput("fOuterRadius2", outerRadius * outerRadius) 
atmo.setShaderInput("fInnerRadius2", earth.getScale().getX() * earth.getScale().getX()) 

atmo.setShaderInput("fKr4PI", 0.000055 * 4 * 3.14159) 
atmo.setShaderInput("fKm4PI", 0.000015 * 4 * 3.14159) 

atmo.setShaderInput("fScale", scale) 
atmo.setShaderInput("fScaleDepth", 0.5) 
atmo.setShaderInput("fScaleOverScaleDepth", scale/0.5) 

# Currently hardcoded in shader 
atmo.setShaderInput("fSamples", 10.0) 
atmo.setShaderInput("nSamples", 10) 

# These do sunsets and sky colors 
# Brightness of sun 
ESun = 15
# Reyleight Scattering (Main sky colors) 
atmo.setShaderInput("fKrESun", 0.000055 * ESun) 
# Mie Scattering -- Haze and sun halos 
atmo.setShaderInput("fKmESun", 0.000015 * ESun) 
# Color of sun 
atmo.setShaderInput("v3InvWavelength", 1.0 / math.pow(0.650, 4), 
                                  1.0 / math.pow(0.570, 4), 
                                  1.0 / math.pow(0.465, 4)) 
                                  
atmo.setShaderInput("v3CameraPos", base.camera.getPos().getX(), 
        base.camera.getPos().getY(), 
        base.camera.getPos().getZ()) 
# Light vector from center of planet.        
lightv = light.getPos() 
lightdir = lightv / lightv.length()

atmo.setShaderInput("v3LightPos", lightdir[0], lightdir[1], lightdir[2]) 
      
atmo.setShaderInput("fCameraHeight", base.camera.getPos().length()) 
atmo.setShaderInput("fCameraHeight2", base.camera.getPos().length()*base.camera.getPos().length()) 

atmo.setShaderInput("g", 0.90) 
atmo.setShaderInput("g2", 0.81) 
atmo.setShaderInput("float", 2) 

atmoShader = Shader.load("shader.cg") 
atmo.setShader(atmoShader) 

def shaderUpdate(task): 
    atmo.setShaderInput("v3CameraPos", base.camera.getPos().getX(), 
        base.camera.getPos().getY(), 
        base.camera.getPos().getZ()) 
    atmo.setShaderInput("fCameraHeight", base.camera.getPos().length()) 
    atmo.setShaderInput("fCameraHeight2", base.camera.getPos().length()*base.camera.getPos().length()) 
    return task.cont 

taskMgr.add(shaderUpdate, 'shaderUpdate') 
run()

shader.cg

//Cg 
// http://stainlessbeer.weebly.com/planets-9-atmospheric-scattering.html 
// http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter16.html 
// http://sponeil.net/ 
float g =-0.90f; 
float g2 = 0.81f; 
float fExposure =.6; 

float scale(float fCos) 
{ 
    float x = 1.0 - fCos;  

	return 0.25 * exp(-0.00287 + x*(0.459 + x*(3.83 + x*(-6.80 + x*5.25))));
	  
 
} 
  
void vshader(float4 vtx_position:POSITION, 
	float4 vtx_color: COLOR,
    in uniform float4 k_v3LightPos,          // The light's current position 
    in uniform float4 k_v3CameraPos,         // The camera's current position 
    in uniform float4 k_v3InvWavelength,     // 1 / pow(wavelength, 4) for RGB 
    in uniform float4 k_fCameraHeight,        // The camera's current height 
    in uniform float4 k_fCameraHeight2,       // fCameraHeight^2 
    in uniform float4 k_fOuterRadius,         // The outer (atmosphere) radius 
    in uniform float4 k_fOuterRadius2,        // fOuterRadius^2 
    in uniform float4 k_fInnerRadius,         // The inner (planetary) radius 
    in uniform float4 k_fInnerRadius2,        // fInnerRadius^2 
    in uniform float4 k_fKrESun,              // Kr * ESun 
    in uniform float4 k_fKmESun,              // Km * ESun 
    in uniform float4 k_fKr4PI,               // Kr * 4 * PI 
    in uniform float4 k_fKm4PI,               // Km * 4 * PI 
    in uniform float4 k_fScale,               // 1 / (fOuterRadius - fInnerRadius) 
    in uniform float4 k_fScaleOverScaleDepth, // fScale / fScaleDepth 
    in uniform float4 k_fScaleDepth,          // The scale depth (i.e. the altitude at which the atmosphere's average density is found) 
    in uniform float4 k_fSamples, 
    in uniform float4 k_nSamples, 
    uniform float4x4 mat_modelproj, 
    out float4 l_position : POSITION, 
    out float4 l_color0   : COLOR0, 
    out float4 l_color1   : COLOR1, 
    out float3 l_v3Direction: TEXCOORD0 ) 
{ 
    float3 v3Pos = vtx_position.xyz; 
    float3 v3Ray = v3Pos - k_v3CameraPos.xyz; 
    float fFar = length(v3Ray); 
    v3Ray /= fFar; 
  
    // Calculate the closest intersection of the ray with the outer atmosphere 
    // (which is the near point of the ray passing through the atmosphere)    
    float B = 2.0 * dot(k_v3CameraPos.xyz, v3Ray); 
    float C = k_fCameraHeight2 - k_fOuterRadius2; 
    float fDet = max(0.0, B*B - 4.0 * C); 
    float fNear = 0.5 * (-B - sqrt(fDet)); 
      
    // Calculate the ray's start and end positions in the atmosphere, 
    // then calculate its scattering offset 
    float3 v3Start = k_v3CameraPos.xyz + v3Ray * fNear; 
    fFar -= fNear; 
    
  
    // Initialize the scattering loop variables 
    float fSampleLength = fFar / 15.0; 
	//float fSampleLength = fFar / 1.0
    float fScaledLength = fSampleLength * k_fScale; 
    float3 v3SampleRay = v3Ray * fSampleLength; 
    float3 v3SamplePoint = v3Start + (v3SampleRay * 0.5); 
    
    float fHeight = length(v3SamplePoint); 
    //float fStartAngle = dot(v3Ray, v3Start) / k_fOuterRadius.x; 
    float fStartAngle = dot(v3Ray, v3Start) / fHeight; 
    //float fStartDepth = exp(-1.0 / k_fScaleDepth.x); 
	float fStartDepth = exp(k_fScaleOverScaleDepth * (k_fInnerRadius - fHeight)); 
    float fStartOffset = fStartDepth * scale(fStartAngle); 
	
    
    // Now loop through the sample points 
    float3 v3FrontColor = float3(0.0, 0.0, 0.0); 
    for(int i=0; i<14; i++)  
    { 
        float fHeight = length(v3SamplePoint); 
		float fDepth = exp(k_fScaleOverScaleDepth * (k_fInnerRadius - fHeight));
        float fLightAngle = dot(k_v3LightPos.xyz, v3SamplePoint) / fHeight; 
        float fCameraAngle = dot(v3Ray, v3SamplePoint) / fHeight; 
        float fScatter = (fStartOffset + fDepth * (scale(fLightAngle) - scale(fCameraAngle))); 
        float3 v3Attenuate = exp(-fScatter *(k_v3InvWavelength.xyz * k_fKr4PI.x + k_fKm4PI.x)); 
        v3FrontColor += v3Attenuate * (fDepth * fScaledLength); 
        v3SamplePoint += v3SampleRay; 
    } 
	l_position=mul(mat_modelproj, vtx_position);  
    l_color0.rgb = v3FrontColor.xyz * k_v3InvWavelength.xyz * k_fKrESun.x; 
    l_color1.rgb=v3FrontColor*k_fKmESun.x; 
    l_v3Direction =k_v3CameraPos.xyz - v3Pos; 
} 
  


void fshader(in float4 l_position : POSITION, 
    in float4 l_color0   : COLOR0, 
    in float4 l_color1   : COLOR1, 
    in float3 l_v3Direction : TEXCOORD0, 
    uniform float4 k_v3LightPos, 
    out float4 o_color:COLOR) 
{ 
    float fCos = dot(k_v3LightPos, l_v3Direction) / length(l_v3Direction); 
    float fRayleighPhase = 0.75 * (1.0 + fCos*fCos); 
	//float fRayleighPhase = .9 * (1.0 + fCos*fCos);
    float fMiePhase= 1.5 * ((1.0 - g2) / (2.0 + g2)) * (1.0 + fCos*fCos) / pow(1.0 + g2 - 2.0*g*fCos, 1.5); 
    //float fMiePhase = 	1.5 * 1.0 / (0.0015 * 4 * 3.14159) * (1.0 - g2) * pow(1.0 + (g2) - 2.0*g*fCos, -3.0/2.0) * (1.0 +fCos * fCos) / (2.0 + g2);
    //This causes a brightness reduction when viewing at 90 degrees 
    float4 col=(fRayleighPhase * l_color0) + (fMiePhase * l_color1); 
    //float4 col=(l_color0) + (fMiePhase * l_color1); 
    //col.a = col.b; 
	col.a=max(col.b,col.r);
    //In some versions, not in others. Not sure what impact it has 
    o_color = 1 - exp(-fExposure * col); 
    //o_color = col; 
}   

Be sure to zoom out if you get it running. The sky is a faint blue. I guess higher-res sphere models may solve the remaining issues, but it’s just a guess.


#5

…and here is a screenshot:


#6

You only need to add one more thing; make the atmosphere fade out. After that, I will totally use this for the space shooter I am working on.


#7

do you mean like this?

The fading effect is small but noticeable. This is using a sphere with many more polygons plus some other changes:
fScaleDepth = 0.25
fScaleOverScaleDepth = scale/0.25

It still does not work when I change the scale from 1.025 to something larger. oh well.


#8

I should thank croxis (op) for the work done in porting the shader code for use with panda3D. Good Job! :smiley:


#9

You are welcome, but you really did an amazing job getting it to work!