Water

A water surface is not seldomly a popular game component. We do not provide directly a water surface component, but it is quite easy to setup one.

The water shader generates the surface by using different textures: DUDV, Normal, Specular, Displacement, Foam and ShoreFoam. By those user specific textures you can control the look of your water.

Besides texturing you'll need additional render pass computation for rendering depth, refraction and reflection.

Water rendering is based on:

  • TGorillaPlane (best results)
  • TGorillaWaterMaterialSource
  • TGorillaRenderPassReflection (Reflections)
  • TGorillaRenderPassRefraction (Depth + Refraction)

Components

Plane

The plane is a simple and opaque instance of TGorillaPlane or TPlane. It will be used as basis mesh to render water onto.

Using a none planar object is not recommended, because reflection and refraction computation is based on plane models.

Refraction

Refraction is the image data simulating underwater 3D objects, modified by some visual effects (Fresnel Effect). We'll need a separated RenderPass for this, to pre-compute the information. That's the reason why there is no all-in-one component for water, because we want users to reuse their renderpasses instead of instanciating multiple renderpasses for the same image data. This would lead to horrible performance issues. It means you could use the refraction renderpass for water and at the same time f.e. for some transparency effect.

Create an instance of TGorillaRenderPassRefraction and apply the viewport and camera to it.

Because we don't want to appear the water surface itself (would be black) in this image data, we should ignore it while refraction computation. This can be done very easily, by:

FRefraction.IgnoreControl(FWaterPlane);

The water surface would be black, because it's image information will be computed in main render pass first.

Because the refraction pass should render from a certain position, we have to adjust the surface position to the current water plane position everytime it changes:

  // set the current position of the water plane as surface plane
  // this needs to be updated, if water plane moves
  FRefraction.SurfacePosition.Point := TPoint3D(FWaterPlane.AbsolutePosition);

Reflection

Reflection is the image data containing a mirrored view on the scene depending on the current camera position and direction. As the name already says, it will be displayed as 3D object reflection on the water surface.

Create an instance of TGorillaRenderPassReflection and apply the viewport and camera to it.

And again we'll need to ignore the water plane while computation, by:

FReflection.IgnoreControl(FWaterPlane);

Because the reflection pass always computes the mirrored camera view, we should tell him, how the virtual mirror surface looks like:

  // for plane vector computation we need to know the size of this mirror plane
  FReflection.MirrorSize := FWaterPlane.Width;
 
  // set the current position of the water plane as mirror plane
  // this needs to be updated, if water plane moves
  FReflection.MirrorPosition.Point := TPoint3D(FWaterPlane.AbsolutePosition);

In case plane size or position changes, you have to update those values in reflection pass.

Displacement

Our water shader supports displacement mapping to render waves. This manipulates vertex position in the vertex shader. To use displacement mapping, apply a tetxure to the “DisplacementMap” property and configure the intensity of waves by the “DisplIntensity” property of your material source.

When working with displacement mapping, vertices getting shifted. This leads to offsets in refraction and reflection, because in those render passes we rendered with different information.

Therefor you have to adjust the reflection mirror position and refraction surface position by the “DisplIntensity” value:

  FReflection.MirrorPosition.Point := TPoint3D(FWaterPlane.AbsolutePosition) + 
    Point3D(0, -FWaterMaterial.DisplIntensity, 0);
 
  FRefraction.SurfacePosition.Point := TPoint3D(FWaterPlane.AbsolutePosition) + 
    Point3D(0, -FWaterMaterial.DisplIntensity, 0);

Water Material

Finally of course we need a material shader (TGorillaWaterMaterialSource), which is able to merge all components together. The material source is inherited from the TGorillaDefaultMaterialSource and supports multiple light sources and the different shading models.

For computing waves, riffles and foam the shader needs a few textures:

  • NormalTexture
  • DUDVTexture
  • DisplacementTexture
  • SpeculareTexture
  • FoamTexture
  • ShoreTexture

The different textures may look like these:

Water normals map Water DUDV map Water displacement map Water specular map Water foam texture

Attention: The image source is unknown! It is not advisable to use these images and, if necessary, to violate the license agreement.

After loading all relevant textures to material source, we only have to apply refraction and reflection to it:

  FWaterMaterial.ReflectionPass := FReflection;
  FWaterMaterial.RefractionPass := FRefraction;

Remarks: normal map texture will also be used as displacement map for simulating real mesh waves, in case no explicit displacement map was applied to the material.

In the end do not forget to link water plane with water material source.

FWaterPlane.MaterialSource := FWaterMaterial;

Properties

Besides the textures, the water material source provides a few configuration properties to influence water look.

Property Description
Diffuse The material shader uses this color as main color for water.
Specular The specular color interacts with the applied specular color map and configurates its intensity.
WaveSpeed Defines how fast waves are moving.
WaveSize Defines the amplitude of waves, which means how high or low waves are simulated. This value do not manipulate mesh vertices (only by displacement mapping).
Displacement Defines the factor of vertex displacement based on the displacement map or normal map.
ReflCorrection By this value you can modify the color of reflection, default value: Vector3D(1.0, 1.0, 1.0, 1.0)
RefrCorrection By this value you can modify the color of refraction on water surface, default value: Vector3D(1.1, 1.1, 1.1, 1.0)
DepthIntensityControl the depth map value intensity while computation. This influences the blending of edges and the mixture between refraction and reflection.
DisplIntensityControl displacement mapping intensity
FoamIntensityControl the color intensity of the applied foam texture.
ShoreIntensityControl the color intensity of the applied shore foam texture.
ShoreWidthIncrease or decrease the size of the shore where the texture affects

Ripples

Since 0.8.3.2265 the framwork supports rendering of water ripples.

Ripple settings are configurable at design time by the following properties, but need runtime implementation to add ripple positions.

PropertyDescription
RipplesActiveActivate or deactivate ripple computation.
RippleAmplificationGet or set amplification value for ripples. Simply said: Amplification describes the height of the ripple.
RippleProximityGet or set proximity value for ripples. Simply said: Proximity is the basis value for the width of a ripple.
RippleDecayGet or set decay value for ripples. Simply said: It defines how fast a ripple will end.

To add a ripple at a certain position use the TGorillaWaterMaterialSource.AddRipple() method.

Warning: The number of ripples is currently hardcoded limited to 32.

procedure TForm1.doOnViewportMouseUp(ASender : TObject; AButton : TMouseButton;
  AShift : TShiftState; X, Y : Single);
var LPt3D : TPoint3D;
begin
  /// add a ripple by clicking with the left mouse button onto the water plane (Caution: HitTest needs to be activated)
  if (ssLeft in fShiftState) then
  begin
    LPt3D := GorillaViewport1.ScreenToWorld(PointF(X, Y));
    GorillaWaterMaterialSource1.AddRipple(Point3D(LPt3D.X, LPt3D.Z, -LPt3D.Y));
  end;
end;

Ripple computation in the vertex shader is embedded inside our water shader program. In case you'll need additional handling in the fragment shader, you could a code snippet like that.

var LStr := TStringList.Create();
try
  LStr.Text := 
	'''
	  void SurfaceShader(inout TLocals DATA){
		float l_TotalRippleEffect = 0.0;
		float l_Time = abs(_TimeInfo.w);
		vec3 l_TransfVertexPos = DATA.TransfVertPos.xyz;
 
		for(int i = 0; i < int(_RippleCount); i++){
		  float l_RippleDist = distance(l_TransfVertexPos.xz, _Ripples[i].xz);
		  float l_RippleTime = abs((_TimeInfo.x + l_Time) - _Ripples[i].w);
		  float l_TimeLimit = clamp(1.0 / (l_RippleTime / (_RippleProximity * _RippleDecay)), 0.0, 1.0);
 
		  float l_RippleEffect = (-_RippleAmplification * exp(_RippleDecay * - l_RippleDist));
		  l_RippleEffect *= cos(_RippleProximity * (l_RippleDist - l_RippleTime));
		  l_RippleEffect *= l_TimeLimit;
 
		  l_TotalRippleEffect += exp(-l_RippleDist) * sin(0.5 * l_RippleDist) * l_RippleEffect;
		}
 
		if(l_TotalRippleEffect > 0.01){
		  vec4 l_BoatTrail = tex2D(_WaterShore, DATA.TexCoord0.xy);
		  DATA.BaseColor.rgb += vec3(l_BoatTrail.rgb * l_TotalRippleEffect);
		  DATA.SumColor.rgb += vec3(l_BoatTrail.rgb * l_TotalRippleEffect);
		}
	  }
	''';
 
  FWaterMat.SurfaceShader := LStr;
finally
  LStr.Free;
end;

This snippet will render the water shore texture onto the ripples if they have a minimum size of 0.01.

Tutorial

Example

Take a look at this example code for a complex water surface setup.

unit Unit1;
 
interface
 
uses
  System.SysUtils,
  System.Types,
  System.UITypes,
  System.Classes,
  System.Variants,
  System.IOUtils,
  System.Math.Vectors,
  FMX.Types,
  FMX.Controls,
  FMX.Forms,
  FMX.Graphics,
  FMX.Dialogs,
  FMX.Types3D,
  FMX.Controls3D,
  FMX.Objects3d,
  FMX.MaterialSources,
  FMX.StdCtrls,
  FMX.Controls.Presentation,
  Gorilla.DefTypes,
  Gorilla.Viewport,
  Gorilla.Skybox,
  Gorilla.Cube,
  Gorilla.Plane,
  Gorilla.Terrain,
  Gorilla.Terrain.Utils,
  Gorilla.Terrain.Algorithm,
  Gorilla.Controller.Passes.Reflection,
  Gorilla.Controller.Passes.Refraction,
  Gorilla.Material.Water,
  Gorilla.Material.Lambert;
 
type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    FGorilla  : TGorillaViewport;
    FLight    : TLight;
    FSkyBox   : TGorillaSkyBox;
    FTerrain  : TGorillaTerrain;
    FTerrainMat : TGorillaLambertMaterialSource;
    FWaterPlane : TGorillaPlane;
    FWaterMat   : TGorillaWaterMaterialSource;
    FReflection : TGorillaRenderPassReflection;
    FRefraction : TGorillaRenderPassRefraction;
    FReflectBody : TGorillaCube;
    FReflectBodyMat : TGorillaLambertMaterialSource;
  public
    { Public-Deklarationen }
  end;
 
var
  Form1: TForm1;
 
implementation
 
{$R *.fmx}
 
uses
  Gorilla.Utils.Math,
  Gorilla.Control,
  Gorilla.Controller,
  Gorilla.Context.Texturing;
 
procedure TForm1.FormCreate(Sender: TObject);
var LPath : String;
begin
  // Get the platform independent assets path
{$IFDEF MSWINDOWS}
  LPath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) +
    IncludeTrailingPathDelimiter('assets');
{$ELSE}
  LPath := IncludeTrailingPathDelimiter(TPath.GetHomePath());
{$ENDIF}
 
  // Create the Gorilla3D viewport
  FGorilla := TGorillaViewport.Create(Self);
  FGorilla.Name := 'GorillaViewport1';
  FGorilla.Color := TAlphaColorRec.Black;
  FGorilla.OnMouseUp := DoOnViewportMouseUp;
  FGorilla.FXAA := 0;
 
  // Create a light source
  FLight := TLight.Create(FGorilla);
  FLight.Parent := FGorilla;
  FLight.Name := 'GorillaLight1';
  FLight.LightType := TLightType.Point;
  FLight.AbsolutePosition := Vector3D(0, -25, -5);
 
  // Skybox to have more reflections
  FSkyBox := TGorillaSkyBox.Create(FGorilla);
  FSkyBox.Parent := FGorilla;
 
  // Terrain with random generation
  FTerrain := TGorillaTerrain.Create(FGorilla);
  FTerrain.Parent := FGorilla;
  fTerrain.BeginUpdate();
  try
    FTerrain.ResolutionX := 128;
    FTerrain.ResolutionY := 128;
    FTerrain.HitTest := false;
    FTerrain.Height := 1;
    FTerrain.Scale.X := 10;
    FTerrain.Scale.Y := 1;
    FTerrain.Scale.Z := 10;
    FTerrain.RandomTerrain(TRandomTerrainAlgorithmType.DiamondSquare, true);
  finally
    FTerrain.EndUpdate();
  end;
 
  // Create a material for the terrain mesh
  FTerrainMat := TGorillaLambertMaterialSource.Create(fTerrain);
  FTerrainMat.Parent := FTerrain;
  TGorillaLambertMaterialSource(FTerrainMat).Texture.LoadFromFile(LPath + 'terrain.jpg');
  FTerrain.MaterialSource := FTerrainMat;
 
  // Water surface plane to render water   
  FWaterPlane := TGorillaPlane.Create(FGorilla);
  FWaterPlane.SetHitTestValue(false);
  FWaterPlane.Parent := FGorilla;
  FWaterPlane.RotationAngle.X := 90;
  FWaterPlane.Width  := 10;
  FWaterPlane.Height := 10;
  FWaterPlane.Position.Y := -0.5;
 
  // High resolution for ripple testing - vertex shader modifies it
  FWaterPlane.SubdivisionsWidth  := 256;
  FWaterPlane.SubdivisionsHeight := 256;
 
  // Water shader material attached to our water surface plane
  FWaterMat := TGorillaWaterMaterialSource.Create(FWaterPlane);
  FWaterMat.Parent := FWaterPlane;
 
  FWaterMat.DisplacementMap.LoadFromFile(LPath + 'water-height.jpg');
  FWaterMat.DisplIntensity := 0.1;
  FWaterMat.NormalMap.LoadFromFile(LPath + 'water-normal.jpg');
  FWaterMat.DUDVTexture.LoadFromFile(LPath + 'water-dudv.jpg');
  FWaterMat.SpecularMap.LoadFromFile(LPath + 'water-specular.jpg');
  FWaterMat.FoamTexture.LoadFromFile(LPath + 'water-foam.jpg');
  FWaterMat.TextureTiling[WATER_TEX_FOAM] := PointF(4, 4);
 
  FWaterMat.ShoreTexture.LoadFromFile(LPath + 'water-shore.png');
  FWaterMat.TextureTiling[WATER_TEX_SHORE] := PointF(2, 2);
 
  // Link material to the water surface plane
  FWaterPlane.MaterialSource := FWaterMat;
 
  // Reflection render pass
  FReflection := TGorillaRenderPassReflection.Create(FGorilla);
  FReflection.Viewport := FGorilla;
 
  // Ignore the water plane, otherwise the rendering might be black
  FReflection.IgnoreControl(FWaterPlane);
  FReflection.MirrorSize := FWaterPlane.Width;
 
  // The mirror surface position is important to to render correctly
  // Because displacement mapping is activated, we need to render reflections
  // with the displacement offset
  FReflection.MirrorPosition.Point := TPoint3D(FWaterPlane.AbsolutePosition) +
    Point3D(0, -FWaterMat.DisplIntensity, 0);
 
  // We do not need fullscreen rendering for reflections, half of the size is enough
  FReflection.ViewportScale := 0.5;
  FReflection.Enabled := true;
 
  // Refraction render pass
  FRefraction := TGorillaRenderPassRefraction.Create(fGorilla);
  FRefraction.Viewport := fGorilla;
 
  // Ignore the water plane, otherwise the rendering might be black
  FRefraction.IgnoreControl(fWaterPlane);
 
  // The mirror surface position is important to to render correctly
  // Because displacement mapping is activated, we need to render reflections
  // with the displacement offset
  FRefraction.SurfacePosition.Point := TPoint3D(FWaterPlane.AbsolutePosition) +
    Point3D(0, -FWaterMat.DisplIntensity, 0);
 
  // We do not need fullscreen rendering for refractions, half of the size is enough
  FRefraction.ViewportScale := 0.5;
  FRefraction.Enabled := true;
 
  // Link reflection and refraction render passes to the material
  FWaterMat.ReflectionPass := fReflection;
  FWaterMat.Reflections := true;
  FWaterMat.ReflectionPower := 1;
  FWaterMat.RefractionPass := fRefraction;
  FWaterMat.Refractions := true;
  FWaterMat.RefractionPower := 1;
 
  // Create a reflective body and material to test reflections / refraction
  FReflectBody := TGorillaCube.Create(FGorilla);
  FReflectBody.Parent := FGorilla;
 
  FReflectBodyMat := TGorillaLambertMaterialSource.Create(FReflectBody);
  FReflectBodyMat.Parent := FReflectBody;
  FReflectBodyMat.UseTexture0 := false;
  FReflectBody.MaterialSource := FReflectBodyMat;
end;
 
end.

Next step: Bokeh