
Applying an animated normal map to a sphere is a problem. You have stretching issues and if using a cube-to-sphere mapping you have uv direction issues. The only solutions I know of to solve these is to do perlin noise in a shader, a 3d repeating noise texture(?) or do tri-planar texturing and use a 2d texture map, which is what I've done here.
I used the code from this GPU Gems 3 article and added a second bump map and animated them by updating the vertex uv coords and the results are decent.

Now there are no visible seams on the sphere and the bump map is pretty evenly spread across the surface. Also when it animates you don't have any visible seams due to opposing uv directions because of the way tri-planar texturing blends the uv maps. This blending does mean, however, that where the cube edges meet on the sphere you get most of the stretching and the bump map is more random. This side affect is OK for water because I want the bumps to look random.
I still have to deal with a bunch of issues including the fps hit this shader brings with it. Now instead of 2 normal map texture look ups per pixel there are 6 - 1 for each of the 3 axis and there are 2 normal maps. On top of this I have texture look ups for the atmosphere color, sun color and the water depth and I haven't even tried adding reflection or refraction.
I've pasted the FX Composer .fx HLSL file at the bottom of this post if you want to try it out.
Also, I have released the Spacescape skybox tool and uploaded the source code to sourceforge.net/projects/spacescape!
The tool is rather complex and so I plan on writing some tutorials & tips to help answer some questions that have been coming up.
Here's an intro video:
Here's the tri-planar sphere.fx file code for NVidia FX Composer. NOTE: this is not my water shader, just the tri-planar stuff and it has a slider called OFFSET that you can drag to see how the bump map animates.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/************* UN-TWEAKABLES **************/ | |
float4x4 WorldITXf : WorldInverseTranspose < string UIWidget="None"; >; | |
float4x4 WvpXf : WorldViewProjection < string UIWidget="None"; >; | |
float4x4 WorldXf : World < string UIWidget="None"; >; | |
float4x4 ViewIXf : ViewInverse < string UIWidget="None"; >; | |
float3 Lamp0Pos : Position < | |
string Object = "PointLight0"; | |
string UIName = "Lamp 0 Position"; | |
string Space = "World"; | |
> = {-0.5f,2.0f,1.25f}; | |
float OFFSET < | |
string UIWidget = "slider"; | |
float UIMin = 0.0; | |
float UIMax = 1.0; | |
float UIStep = 0.001; | |
string UIName = "Offset"; | |
> = 0.0; | |
texture normalTexture < | |
string ResourceName = "water-normal.png"; | |
string ResourceType = "2D"; | |
string UIName = "Water Normal Texture"; | |
>; | |
sampler2D NormalSampler = sampler_state | |
{ | |
Texture = <normaltexture>; | |
Filter = MIN_MAG_MIP_LINEAR; | |
AddressU = Wrap; | |
AddressV = Wrap; | |
}; | |
/************* DATA STRUCTS **************/ | |
/* data from application vertex buffer */ | |
struct appdata { | |
float3 Position : POSITION; | |
}; | |
/* data passed from vertex shader to pixel shader */ | |
struct vertexOutput { | |
float4 HPosition : POSITION; | |
float3 UV : TEXCOORD0; | |
float3 UV2 : TEXCOORD1; | |
float3 WorldNormal : TEXCOORD2; | |
float3 LightVec : TEXCOORD3; | |
}; | |
/*********** vertex shader ******/ | |
vertexOutput simpleVS(appdata IN) { | |
vertexOutput OUT = (vertexOutput)0; | |
float4 Po = float4(IN.Position.xyz,1); | |
float3 Pw = mul(Po,WorldXf).xyz; | |
OUT.HPosition = mul(Po, WvpXf); | |
OUT.LightVec = (Lamp0Pos - Pw); | |
OUT.WorldNormal = Pw; | |
// OFFSET is used to preview what animation would look like | |
// first uv for large waves | |
OUT.UV = ((Pw + 1.0) * 0.5); | |
OUT.UV.xy +=OFFSET; | |
// second uv for small waves | |
OUT.UV2 = ((Pw + 1.0) * 0.5); | |
OUT.UV2.xz +=(OFFSET * 2.0); | |
return OUT; | |
} | |
/********* pixel shader ********/ | |
float4 simplePS(vertexOutput IN) : COLOR { | |
float3 blend_weights = abs( IN.WorldNormal ); // Tighten up the blending zone: | |
blend_weights = (blend_weights - 0.2) * 7; | |
blend_weights = max(blend_weights, 0); // Force weights to sum to 1.0 (very important!) | |
blend_weights /= (blend_weights.x + blend_weights.y + blend_weights.z ).xxx; | |
float4 blended_color; // .w hold spec value | |
float3 blended_bump_vec; | |
// Compute the UV coords for each of the 3 planar projections. | |
// tex_scale (default ~ 1.0) determines how big the textures appear. | |
float tex_scale = 4.0; | |
float tex_scale2 = 8.0; | |
float2 coord1 = IN.UV.yz * tex_scale; | |
float2 coord2 = IN.UV.zx * tex_scale; | |
float2 coord3 = IN.UV.xy * tex_scale; | |
float2 coord4 = IN.UV2.yz * tex_scale2; | |
float2 coord5 = IN.UV2.zx * tex_scale2; | |
float2 coord6 = IN.UV2.xy * tex_scale2; | |
// Sample color maps for each projection, at those UV coords. | |
float4 col1 = float4(1.0,0,0,1); | |
float4 col2 = float4(0,1.0,0,1); | |
float4 col3 = float4(0,0,1.0,1); | |
// Sample bump maps too, and generate bump vectors. | |
float2 bumpFetch1 = tex2D(NormalSampler,coord1).xy - 0.5; | |
float2 bumpFetch2 = tex2D(NormalSampler,coord2).xy - 0.5; | |
float2 bumpFetch3 = tex2D(NormalSampler,coord3).xy - 0.5; | |
float2 bumpFetch4 = tex2D(NormalSampler,coord4).xy - 0.5; | |
float2 bumpFetch5 = tex2D(NormalSampler,coord5).xy - 0.5; | |
float2 bumpFetch6 = tex2D(NormalSampler,coord6).xy - 0.5; | |
// (Note: this uses an oversimplified tangent basis.) | |
float3 bump1 = float3(0, bumpFetch1.x, bumpFetch1.y); | |
float3 bump2 = float3(bumpFetch2.y, 0, bumpFetch2.x); | |
float3 bump3 = float3(bumpFetch3.x, bumpFetch3.y, 0); | |
float3 bump4 = float3(0, bumpFetch4.x, bumpFetch4.y); | |
float3 bump5 = float3(bumpFetch5.y, 0, bumpFetch5.x); | |
float3 bump6 = float3(bumpFetch6.x, bumpFetch6.y, 0); | |
// Finally, blend the results of the 3 planar projections. | |
blended_color = col1.xyzw * blend_weights.xxxx + | |
col2.xyzw * blend_weights.yyyy + | |
col3.xyzw * blend_weights.zzzz; | |
blended_bump_vec = bump1.xyz * blend_weights.xxx + | |
bump2.xyz * blend_weights.yyy + | |
bump3.xyz * blend_weights.zzz; | |
blended_bump_vec += (bump4.xyz * blend_weights.xxx + | |
bump5.xyz * blend_weights.yyy + | |
bump6.xyz * blend_weights.zzz) * 0.5; | |
float3 N_for_lighting = normalize(IN.WorldNormal + blended_bump_vec); | |
return saturate(blended_color) * dot(normalize(IN.LightVec),N_for_lighting); | |
} | |
/*************/ | |
technique main | |
{ | |
pass p0 | |
{ | |
VertexShader = compile vs_3_0 simpleVS(); | |
PixelShader = compile ps_3_0 simplePS(); | |
ZEnable = true; | |
ZWriteEnable = true; | |
CullMode=None; | |
} | |
} | |
/***************************** eof ***/ |