Wednesday, May 5, 2010

Planet Terrain Shader With Atmosphere

Before you read this post, if you have not watched Ysaneya's latest tech demo video go watch it now! It is amazing!

In the previous posts I described and published my latest atmosphere shader and I got some useful feedback and requests for information, which I thought I should include here:

- The atmosphere shader is applied to an inverted sphere that is drawn before the planet. This means that if you have a single atmosphere shell and a planet with no atmosphere shader added to it, then you will just see the atmosphere effect behind the planet. I go into how I've added worked in atmosphere shader code into my terrain shader later on in the post you are reading now.

- In the atmosphere shader the camera and light should be in object space, as should the vertices.

- The atmosphere is still not fully opaque when on the sunny side. It is opaque (not see through) at the horizon and gets slightly transparent as it approaches the sun so some stars still show through which may not be desired. I plan on addressing this later when I add other planets and moons. I'm concerned that if I make the atmosphere too opaque then you won't see the moon or planet (or large space ships!) out there, and if I make the atmosphere too transparent then you'll see too many stars on the day side. I may just end up darkening the skybox as the user gets on the daylight side of the planet, but I haven't gotten there yet.

Now, about the planet terrain. I create an 8 way blend look up table based on terrain slope and height. When the planet is first created I render to texture a medium resolution texture (512x512 currently) that blends 8 textures based on this look up table. When viewing the planet from far away I display this medium resolution texture, then when the camera approaches the surface I blend between this medium resolution texture and a shader that actually blends the 8 textures per frame, which is slower but looks better close up.

Too add the atmosphere effect to my terrain in the vertex shader I basically calculate the amount of atmosphere fog like this:
// atmosphere fog / haze attenuation
float3 camToPos = position.xyz- camPos.xyz;
float camToPosDist = length(camToPos);
float visibilityDistance = min(AtmosphereHeight,VisibilityDistance);
oAttenuation = saturate((camToPosDist - VisibilityDistance)/ (AtmosphereHeight + visibilityDistance));


position - The vertex position in object space
camPos - The camera position in object space
AtmosphereHeight - Atmosphere sphere radius minus terrain sphere radius.
VisibilityDistance - At distance = 0, fog is 0 at distance = VisibilityDistance, fog = 1

In my fragment shader I do this:
// gradient0 is a look up table like atmosphere gradient but two pixels high. 
// Top row is atmosphere gradient and bottom row is sun color gradient
float4 sunColor = tex2D(gradient0,float2(uv2,1));
float4 atmosphereColor = tex2D(gradient0,float2(uv2,0));

// how transparent is the atmosphere?
float transparency = min(AtmosphereTransparency,attenuation); 

// get diffuse amount from 8 way texture blend and apply lighting with normals
// or shadow map/light map
...

// get atmosphere contribution    
float3 atmosphereAmt = (atmosphereColor * sunColor * (transparency + (transparency * shadow)));

// add in atmosphere
outColor.rgb = (diffuse.rgb * sunColor * (1.0 - transparency)) + atmosphereAmt;


I use a constant called AtmosphereTransparency to make sure that the atmosphere contribution is no more than a certain amount. I use 0.3 because it looks OK, but on a really dense atmosphere you might want a higher amount.

Here's the full terrain shader you see in the videos when the camera is on the surface - this is the one I use for Ogre3d not for FX Composer.

void main_vp( float4 position : POSITION,
float2 uv       : TEXCOORD0,

out float4 oPosition : POSITION,
out float2 oUV1    : TEXCOORD0,
out float3 oLightDir : TEXCOORD1,
out float  oBlendAmt : TEXCOORD2,
out float  oUV2    : TEXCOORD3,
out float  oAttenuation: TEXCOORD4,

uniform float4x4 worldViewProjMatrix,
uniform float4 camPos,
uniform float4 lightPos,
uniform float AtmosphereHeight,
uniform float VisibilityDistance     
)
{
oPosition = mul(worldViewProjMatrix, position);
oUV1 = uv;

// directional light
oLightDir = normalize(lightPos.xyz);

// dot product of position and light in range 0..1
float posLength = length(position.xyz);
float3 normal = position.xyz / posLength;
oUV2 = (dot(oLightDir, normal) + 1.0) * 0.5;

// atmosphere fog / haze attenuation
float3 camToPos = position.xyz- camPos.xyz;
float camToPosDist = length(camToPos);
float visibilityDistance = min(AtmosphereHeight,VisibilityDistance);
oAttenuation = saturate((camToPosDist - VisibilityDistance)/ (AtmosphereHeight + visibilityDistance));

// start blending between low res texture and high at 512 units out
float blendStart = 512.0;
float blendEnd = 0.0;
float blendDistance = blendStart - blendEnd;

oBlendAmt = max(0.0, camToPosDist - blendEnd);
oBlendAmt = min(1.0, oBlendAmt / blendDistance); 
}


And here is the fragment shader:
sampler diffTex0 : register(s0);
sampler diffTex1 : register(s1);
sampler diffTex2 : register(s2);
sampler diffTex3 : register(s3);
sampler diffTex4 : register(s4);
sampler diffTex5 : register(s5);
sampler diffTex6 : register(s6);
sampler diffTex7 : register(s7);
sampler blend0 : register(s8);
sampler blend1 : register(s9);
sampler diffuse0 : register(s10);
sampler gradient0 : register(s11);

float4 getBlendedSample8(in float4 weights0, in float4 weights1, in float2 diffUV) 
{ 
// use w,x,y,z order because PNG uses pixel format A8R8G8B8
return tex2D(diffTex0, diffUV)  * weights0.w +
tex2D(diffTex1, diffUV)  * weights0.x +
tex2D(diffTex2, diffUV)  * weights0.y +
tex2D(diffTex3, diffUV)  * weights0.z +
tex2D(diffTex4, diffUV)  * weights1.w +
tex2D(diffTex5, diffUV)  * weights1.x +
tex2D(diffTex6, diffUV)  * weights1.y +
tex2D(diffTex7, diffUV)  * weights1.z;
}

void main_fp(
float2 uv : TEXCOORD0,
float3 lightDir : TEXCOORD1,
float  blendAmt : TEXCOORD2,
float  uv2 : TEXCOORD3, 
float  attenuation : TEXCOORD4,

uniform float AtmosphereTransparency,
uniform float tileFactor,

out float4 outColor : COLOR
)
{
float4 sunColor = tex2D(gradient0,float2(uv2,1));
float4 atmosphereColor = tex2D(gradient0,float2(uv2,0));

// how transparent is the atmosphere?
float transparency = min(AtmosphereTransparency,attenuation); 

// get the texture color and lit value
float4 diffuse = tex2D(diffuse0, uv);
float shadow = diffuse.a;

// FYI YOU MAY NOT WANT TO DO THIS IN YOUR SHADER
// make ambient dark on side where light is
// and light on the side where it is dark
float3 ambient = sunColor * (1.0 - uv2);

float4 blended =  getBlendedSample8(tex2D(blend0, uv), tex2D(blend1, uv), uv * tileFactor);
diffuse = lerp(blended, diffuse, blendAmt);

// now shade based on the normal
diffuse.rgb = ((1.0 - shadow) * diffuse.rgb * ambient) + (diffuse.rgb * shadow);

// get atmosphere contribution    
float3 atmosphereAmt = (atmosphereColor * sunColor * (transparency + (transparency * shadow)));    

// add in atmosphere
outColor.rgb = (diffuse.rgb * sunColor * (1.0 - transparency)) + atmosphereAmt;
outColor.a = 1.0;
}