Research Coding Projects 3D

Water Geometry Shader



A HLSL geometry shader that creates a water effect (Shader Model 4.0 implemented in DirectX 10).
The base mesh is a flat, unanimated plane.
The plane is then tesselated and animated by the GPU.
I'll demonstrate the shader inside Nvidia FX Composer.


  • Fx Composer
  • Custom Framework


  • C++
  • HLSL

Download Paper Download Paper


image carousel

Some Details

General Info

In my first attempt at creating my geometry shader, I merely wanted to make a Wave Shader.
I succeeded, but I wasn’t satisfied with the results.
I had to make it more realistic.
A wave is a basic mathematical equation with following variables:
  • Wave Height or Amplitude
  • Wave Frequency
  • Wave Phase
  • Wave Length
  • Optional Wave Offset

Shader stages

Our vertex shader is the first shader stage we encounter,
nothing is done at this stage only returning the incomming data.

// Vertex shader
	return vsData;

Next shader stage is the geometry shader, before we start we need to define a maximum vertex count that can be outputted by the stage.
I decided to use 50 because we cannot exceed the max amount of output of the geometry shader (1024).
Our Geometry shader will get a GSDATA structure as input.
This structure will look something like this:

// Geometry Shader Data
struct GSDATA
	float4 Position : SV_POSITION;
	float3 Normal : NORMAL;
	float3 Color : COLOR0;
	float3 Tangent : TANGENT;
	float2 TexCoord : TEXCOORD0;
	float4 worldPosition : COLOR1;

You can see that our GSDATA stucture contains a float4, float3, float3, float3, float2 and float4 together they count 19 floats.
If we multiply that number times 50 (number of vertices we decided) our result will be 950.
This is nearly our limit. In fact we can output some more vertices, to be specific: 53 vertices,
but to keep it clean I used 50 because it's a round number.
The geometry shader stage is mostly used to tesselate our incomming geometry

To tesselate our triagle we need to have the average of each side of the triangle then recreate a new triangle with the calculated vertices.
However, keep in mind that we don’t need only the average of the position of the vertices,
the average of all variables in VSDATA struct have to be computed.
We will use a method to create our triangles, this method will create a new GSDATA sturcture and append it to the tristream.
Once all the data is calculated they can be made to behave like a wave, by applying the formula:

//Evaluate Wave
float EvaluateWave(float x, float amplitude)
	return amplitude * sin (((2*PI)*m_Frequency)*m_Timer - ((2*PI)/m_WaveLenght)*x + m_Phase) + m_Offset;

The final stage of our shader is the pixel shader stage.
The first thing we need to do is normalize our normal and tangent data to counter interpolation.
After that, view direction is computed by normalizing the difference between the world position and the third row of the inverse view matrix also known as the camera position.
First step in determining the normal is computing the binormal.
This is done by calculating the cross-product of the tangent and the original normal determined by gsData.
With the binormal a local axis can be assembled that will be used to multiply with the sampled normal.
The sampled normal is obtained by multiplying both normal map texture lookups.
Next we calculate the specular, the cubemap reflection, fresnel color
After all computing steps are the results are summed:

//Final Color
float4 outColor = float4(diffuseColor + specularColor + enviromentColor + Fresnel,m_Opacity);

A final remark on this shader is that there is room for improvement like:
  • Including geometry collision with terrain meshes to have crashing waves.
  • Adding splashing water geometry
  • Usage of diffrent equations for wave generation